Repository navigation
fix(serve-instrument): keep the server running when a rebuild or request fails - #1851
Open
Sanilas-ai wants to merge 1 commit into
Open
Sanilas-ai wants to merge 1 commit into
Sanilas-ai wants to merge 1 commit into
Conversation
…est fails Deleting or renaming an instrument directory while serve-instrument ran, or requesting a URL with a malformed percent-escape, rejected a promise nothing handled, and Node exited the process. In --all mode that took down every other instrument too. Read the instrument directory inside the rebuild's try and clear the cached bundle when it fails, so a deleted instrument answers 404 (503 in single mode) instead of its last bundle. Log a watcher 'error' (Windows reports a deleted directory as EPERM) and drop the bundle so the next request rebuilds. Catch a rejected request handler, log it and answer 500, and render each page before writing its head so that catch can always respond. Answer 400 to a malformed escape in an instrument URL. Closes DouglasNeuroInformatics#1804
joshunrau
requested changes
Nov 4, 2026
joshunrau
left a comment
Collaborator
There was a problem hiding this comment.
Thanks for this. It's a careful fix, and it follows the plan on #1804 closely, including the stale-bundle and watcher 'error' points. I ran your four new tests against main's server.tsx, and each one fails with the crash the issue describes. When I removed either encodedBundle = null, or moved the single-mode writeHead back above the render, a different test caught each change.
One change before merge:
- Please also test the 500 path in
--allmode. The render-before-writeHeadchange is tested only in single mode. I movedres.writeHead(200)back aboveawait this.renderPage(...)on the--allindex page and on the instrument page (server.tsxlines 253-255 and 276-283), and all 27 server tests still passed. With that order, a failed render makes the.catchcallwriteHead(500)after the head has already been written. That throwsERR_HTTP_HEADERS_SENTinside the.catchand the process exits. That is the crash this PR fixes, in the mode where it takes down every instrument. To cover it, add an--allcounterpart toshould answer 500 and keep serving when handling a request fails. After a first successful request (so the instrument bundle is already cached, as in the single-mode test), make theclient.jsread reject once on/and once on/forms/<name>. Assert a 500, then a 200 on the next request.
Reviewed at commit 26389e3.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1804.
serve-instrumentexited when an instrument directory was deleted or renamed while it ran, or when it got a URL with a malformed percent-escape. In both cases a promise nobody handled was rejected, and Node turned that into a process exit. In--allmode that took down every instrument.Changes
packages/serve-instrument/src/server.tsx:updateEncodedBundlenow does thereaddir/readFilecalls inside itstry. ItscatchsetsencodedBundle = null, so a deleted instrument answers 404 (503 in single mode) and doesn't keep serving its last bundle, as the triage comment pointed out.InstrumentLoaderwatcher has an'error'listener. It logs the error and clears the bundle, so the next request rebuilds. This covers the WindowsEPERM-on-delete case.Server's request callback catches a rejectedhandle, logs it and answers 500.writeHeadafterwards. Everyawaitin bothhandlemethods now runs beforewriteHead, so the.catchcan always send a 500.URIErrorfromdecodeURIComponentis caught where it is thrown and answers 400.logFailurehelper, so the non-Errorbranch is covered once.Docs:
packages/serve-instrument/AGENTS.mdhas a new trap about the never-reject-unhandled / render-before-writeHeadrule, and the test notes now describe the new mock..agents/docs/architecture/testing-strategy.md: fixed theserve-instrumentrow. The mock lives insrc/__tests__/server.test.ts, andServer.stop()does close the watchers. build: upgrade dependencies held back by breaking changes #1660 merged with the old text still there.Verification
src/__tests__/server.test.ts: thenode:fsmock now records each watcher's target and callback and returns anEventEmitterwithclose. There are 4 new tests:forms/BARand firing its callback: FOO still answers 200 and BAR answers 404/forms/%E0%A4%Aanswers 400 and/still answers 200client.jsread rejects once: the response is 500, the error is logged, and the next request answers 200'error': the error is logged and the next request callsbundleagainmain'sserver.tsx, all 4 new tests fail (unhandledENOENT/URIErrorrejections and timeouts). With this change, all 46 tests in theserve-instrumentproject pass, andserver.tsx,cli.ts,client.tsxandroot.tsxare at 100% statements/branches/functions/lines.pnpm --filter @opendatacapture/serve-instrument lint(tsc + eslint) passes.dist/cli.js --alland two forms. Afterrm -rf forms/BARthe server stays up: FOO answers 200, BAR answers 404,/forms/%E0%A4%Aanswers 400 and/answers 200.What I couldn't run:
serve-instrumentisn't part of the stack undertesting/, as the issue says.pnpm test:coverage/pnpm test:e2e. My machine's CPU has no AVX, so MongoDB (andmongodb-memory-serverin the api tests) dies withSIGILL. CI will run the full suite.