fix: db volume ownership and explicit error handling for write failures
All checks were successful
CI / test (pull_request) Successful in 9m32s

Root cause of the 500 on create/update/delete: the non-root app user in
the Docker container lacked write permission to the volume mount point.
Docker volume mounts are owned by root by default; the app user (added
in a previous commit) could read the database but not write to it.

Fixes:

1. Dockerfile — RUN mkdir -p /app/data before chown so the directory
   exists in the image with correct ownership. Docker uses this as a
   seed when initialising a new named volume, ensuring the app user
   owns the mount point from the start.

   NOTE: existing volumes from before the non-root user was introduced
   will still be root-owned. Fix with:
     docker run --rm -v catalyst-data:/data alpine chown -R 1000:1000 /data

2. server/routes.js — replace bare `throw e` in POST/PUT catch blocks
   with console.error (route context + error) + explicit 500 response.
   Add try-catch to DELETE handler which previously had none. Unexpected
   DB errors now log the route they came from and return a clean JSON
   body instead of relying on the generic Express error handler.

3. server/db.js — wrap the boot init() call in try-catch. Fatal startup
   errors (e.g. data directory not writable) now print a clear message
   pointing to the cause before exiting, instead of a raw stack trace.

TDD: tests written first (RED), then fixed (GREEN). Six new tests in
tests/api.test.js verify that unexpected DB errors on POST, PUT, and
DELETE return 500 with { error: 'internal server error' } and call
console.error with the route context string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-28 11:11:00 -04:00
parent 7a5b5d7afc
commit 15ed329743
4 changed files with 94 additions and 7 deletions

View File

@@ -78,7 +78,8 @@ router.post('/instances', (req, res) => {
} catch (e) {
if (e.message.includes('UNIQUE')) return res.status(409).json({ error: 'vmid already exists' });
if (e.message.includes('CHECK')) return res.status(400).json({ error: 'invalid field value' });
throw e;
console.error('POST /api/instances', e);
res.status(500).json({ error: 'internal server error' });
}
});
@@ -98,7 +99,8 @@ router.put('/instances/:vmid', (req, res) => {
} catch (e) {
if (e.message.includes('UNIQUE')) return res.status(409).json({ error: 'vmid already exists' });
if (e.message.includes('CHECK')) return res.status(400).json({ error: 'invalid field value' });
throw e;
console.error('PUT /api/instances/:vmid', e);
res.status(500).json({ error: 'internal server error' });
}
});
@@ -112,6 +114,11 @@ router.delete('/instances/:vmid', (req, res) => {
if (instance.stack !== 'development')
return res.status(422).json({ error: 'only development instances can be deleted' });
deleteInstance(vmid);
res.status(204).end();
try {
deleteInstance(vmid);
res.status(204).end();
} catch (e) {
console.error('DELETE /api/instances/:vmid', e);
res.status(500).json({ error: 'internal server error' });
}
});