Skip to content

Node adapter aborts requests after the socket closes, triggering Undici “Response body … disturbed” crash #142

@songkeys

Description

@songkeys

Summary

When a client connection closes before any handler accesses request.signal, the Node adapter will lazily instantiate the internal AbortController while handling the close event. At that point the underlying ReadableStream has already been disturbed/locked, so Undici throws TypeError: Response body object should not be disturbed or locked and the dev server exits.

Steps to reproduce

  1. Start a Node server that uses srvx/node (e.g., via TanStack Start or Nitro).
  2. Trigger any route that streams a response (SSE, large file, etc.).
  3. Abort the HTTP connection immediately (close the browser tab or curl with --max-time 1).
  4. Observe the server crash:
    node:internal/deps/undici/undici:15845
          Error.captureStackTrace(err);
                ^
    
    TypeError: Response body object should not be disturbed or locked
        at node:internal/deps/undici/undici:15845:13
    

Root cause

In src/adapters/node.ts, the NodeServer handler creates a NodeRequest and only instantiates the abort controller when request.signal is accessed. The server also listens for nodeRes/socket close events and unconditionally calls req._abortController.abort(...). On a premature client disconnect, that call lazily constructs the controller after Node has already disturbed the body stream, so Undici throws before any of our code runs.

Hono hit the same bug recently (see honojs/node-server#221) and fixed it by:

  • Exporting the internal abortControllerKey symbol from the request implementation.
  • Checking whether the controller exists before calling abort() inside the close handler.

Proposed fix

  1. Export the internal controller symbol from src/adapters/_node/request.ts (or expose a helper) so other modules can tell whether the controller was initialized.
  2. In src/adapters/node.ts, register a nodeRes.on('close') listener that retrieves req[abortControllerKey] and returns early if it is undefined. Only call abort() when a controller already exists, and reuse the same error messages used today.
  3. Add a regression test similar to Hono’s “should handle request abort without requestCache” case to ensure future changes don’t reintroduce the bug.

With that guard in place, aborted client connections simply stop the response without crashing the Node process.

Happy to submit a PR if the above approach sounds good.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions