Uncatchable AssertionError: assert(!this.paused) on socket end (original) (raw)
Description
If the consumer doesn't read the body immediately, the HTTP/1 parser pauses under
backpressure. When the socket ends (FIN), onHttpSocketEnd calls parser.finish(),
which asserts !this.paused. Since the parser is paused, this throws an uncatchableAssertionError from the socket 'end' handler and crashes the process.
AssertionError [ERR_ASSERTION]: assert(!this.paused)
at Parser.finish (.../undici/lib/dispatcher/client-h1.js:374:5)
at Socket.onHttpSocketEnd (.../undici/lib/dispatcher/client-h1.js:951:30)
onHttpSocketEnd doesn't check parser.paused before calling finish().
Reproduction (minimised with Claude Code)
import { createServer } from "node:net"; import { fetch } from "undici";
const BODY = Buffer.alloc(64 * 1024, 0x61);
const server = createServer((sock) => {
sock.once("data", () => {
sock.write(
"HTTP/1.1 200 OK\r\n" +
Content-Length: ${BODY.length}\r\n +
"Connection: close\r\n\r\n",
);
sock.write(BODY);
sock.end(); // FIN
});
});
server.listen(0, "127.0.0.1", async () => {
const { port } = server.address();
const res = await fetch(http://127.0.0.1:${port}/);
void res; // never read the body -> parser pauses -> crash on FIN
await new Promise((r) => setTimeout(r, 1000));
console.log("no crash"); // never reached
});
The 64 KiB body is the body highWaterMark — the smallest size that pauses while still
arriving in one socket read, which makes this deterministic. It's not a fixed value:
the real trigger is "paused with the read buffer drained when FIN arrives," so the
size is network-dependent (intermittent across sizes over a real connection).
Expected vs actual
- Expected: clean end, or a catchable error. A peer closing a non-keep-alive
response under backpressure shouldn't crash the process. - Actual: uncatchable
AssertionError, exit code 1.
Environment
- undici 8.3.0 (and current
main) - Node.js v22.22.2, Linux