Skip to content

serve-instrument exits when an instrument directory is deleted or a request URL has a malformed percent-escape #1804

Description

@joshunrau

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

  1. 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.
  2. 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.
  3. 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.

Activity

  1. added
    BugType: existing behavior is wrong
    Priority: LowNice to have, cosmetic or speculative
    Difficulty: LowIsolated change, about 2 hours or less
    Area: InstrumentsInstrument authoring API, bundling, runtime, rendering, measures and the built-in library
    Good First IssueWell-scoped, low-risk and self-contained; a good entry point for a new contributor
    on Oct 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area: InstrumentsInstrument authoring API, bundling, runtime, rendering, measures and the built-in libraryBugType: existing behavior is wrongDifficulty: LowIsolated change, about 2 hours or lessGood First IssueWell-scoped, low-risk and self-contained; a good entry point for a new contributorPriority: LowNice to have, cosmetic or speculative

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions