serve-instrument exits with an unhandled error when an instrument directory is deleted or renamed while it runs, or when it receives a URL with a malformed percent-escape. In both cases an exception escapes an async function that is called with void: this.handler.handle(...) for requests, and this.updateEncodedBundle() in the fs.watch callback. Node turns the unhandled rejection into a process exit. In updateEncodedBundle, only the bundle() call is inside the try; the readdir/readFile before it are not. In --all mode, deleting or renaming one instrument folder kills the server for all the others, and so does a git checkout that removes one. An author has to notice that the terminal died and restart it.
Where
packages/serve-instrument/src/server.tsx:45-48, 62-79 and 243-247, 312:
this.watcher = fs.watch(target, { recursive: false }, () => {
// ...
void this.updateEncodedBundle();
});
// ...
private async updateEncodedBundle(): Promise<void> {
const inputs: BundlerInput[] = [];
const filepaths = await fs.promises
.readdir(this.target, 'utf-8')
// ...
for (const filepath of filepaths) {
const loader = inferLoader(filepath);
const content: string | Uint8Array = await fs.promises.readFile(filepath, /* ... */);
// ...
}
const start = Date.now();
try {
// ...
const decodedName = decodeURIComponent(name!);
// ...
this.server = http.createServer((...args) => void this.handler.handle(...args));
Reproduce
- Build it (
pnpm --filter @opendatacapture/serve-instrument build) and run node dist/cli.js --all <dir> on a directory containing forms/FOO/index.ts and forms/BAR/index.ts.
- Open
/forms/BAR, then rm -rf <dir>/forms/BAR.
Actual: ↺ [forms/BAR] File changed, rebuilding..., then Error: ENOENT: no such file or directory, scandir '…/forms/BAR' and the process exits (verified on main). /forms/FOO is no longer served.
- Restart, then
curl 'http://localhost:3000/forms/%E0%A4%A'.
Actual: URIError: URI malformed at decodeURIComponent and the process exits (verified on main).
Expected: the server logs the failure and keeps running. A deleted instrument answers 404 and a malformed URL answers 400.
Tests
packages/serve-instrument/src/__tests__/server.test.ts:
it('should keep serving other instruments after an instrument directory is deleted, so a rebuild failure does not kill the server'): fire the captured watch callback after removing the temp directory, and assert that the remaining instrument still returns 200.
it('should answer 400 to a malformed percent-escape in an instrument URL rather than crashing')
No e2e test applies: serve-instrument is not part of the stack under testing/.
Suggested fix
Move the directory listing and file reads inside the existing try in updateEncodedBundle. Wrap handle in Server's request callback with a .catch that logs the error and ends the response with 500 if headers are not yet sent. Map a URIError from decodeURIComponent to 400.
serve-instrumentexits with an unhandled error when an instrument directory is deleted or renamed while it runs, or when it receives a URL with a malformed percent-escape. In both cases an exception escapes an async function that is called withvoid:this.handler.handle(...)for requests, andthis.updateEncodedBundle()in thefs.watchcallback. Node turns the unhandled rejection into a process exit. InupdateEncodedBundle, only thebundle()call is inside thetry; thereaddir/readFilebefore it are not. In--allmode, deleting or renaming one instrument folder kills the server for all the others, and so does agit checkoutthat removes one. An author has to notice that the terminal died and restart it.Where
packages/serve-instrument/src/server.tsx:45-48,62-79and243-247,312:Reproduce
pnpm --filter @opendatacapture/serve-instrument build) and runnode dist/cli.js --all <dir>on a directory containingforms/FOO/index.tsandforms/BAR/index.ts./forms/BAR, thenrm -rf <dir>/forms/BAR.Actual:
↺ [forms/BAR] File changed, rebuilding..., thenError: ENOENT: no such file or directory, scandir '…/forms/BAR'and the process exits (verified onmain)./forms/FOOis no longer served.curl 'http://localhost:3000/forms/%E0%A4%A'.Actual:
URIError: URI malformedatdecodeURIComponentand the process exits (verified onmain).Expected: the server logs the failure and keeps running. A deleted instrument answers 404 and a malformed URL answers 400.
Tests
packages/serve-instrument/src/__tests__/server.test.ts:it('should keep serving other instruments after an instrument directory is deleted, so a rebuild failure does not kill the server'): fire the captured watch callback after removing the temp directory, and assert that the remaining instrument still returns 200.it('should answer 400 to a malformed percent-escape in an instrument URL rather than crashing')No e2e test applies:
serve-instrumentis not part of the stack undertesting/.Suggested fix
Move the directory listing and file reads inside the existing
tryinupdateEncodedBundle. WraphandleinServer's request callback with a.catchthat logs the error and ends the response with 500 if headers are not yet sent. Map aURIErrorfromdecodeURIComponentto 400.