-
Notifications
You must be signed in to change notification settings - Fork 35
Description
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
- Start a Node server that uses
srvx/node(e.g., via TanStack Start or Nitro). - Trigger any route that streams a response (SSE, large file, etc.).
- Abort the HTTP connection immediately (close the browser tab or
curlwith--max-time 1). - 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
abortControllerKeysymbol from the request implementation. - Checking whether the controller exists before calling
abort()inside the close handler.
Proposed fix
- 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. - In
src/adapters/node.ts, register anodeRes.on('close')listener that retrievesreq[abortControllerKey]and returns early if it isundefined. Only callabort()when a controller already exists, and reuse the same error messages used today. - 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.