Skip to content

fix(ext/node): cancelling Readable.toWeb(req) no longer destroys the socket#33570

Merged
bartlomieju merged 2 commits into
mainfrom
fix/readable-toweb-cancel-socket
Apr 27, 2026
Merged

fix(ext/node): cancelling Readable.toWeb(req) no longer destroys the socket#33570
bartlomieju merged 2 commits into
mainfrom
fix/readable-toweb-cancel-socket

Conversation

@bartlomieju

Copy link
Copy Markdown
Member

Fixes #33567

Summary

  • The HTTP rewrite (feat(ext/node): rewrite node:http with llhttp and native TCPWrap #33208) ported Node.js's IncomingMessage._destroy(), which destroys the underlying socket when the request body is not fully consumed. The webstreams adapter's cancel() callback called destroy() directly, bypassing the destroyer() helper that Node.js uses.
  • In Node.js, destroyer() detects server requests (via isServerRequest()) and nullifies the socket reference before calling destroy(), so _destroy() skips socket teardown. This keeps the connection alive for the response.
  • Uses destroyer() instead of destroy() in newReadableStreamFromStreamReadable's cancel callback, matching Node.js's behavior.

Test plan

  • Added unit test in tests/unit_node/http_test.ts that creates an HTTP server, converts the request to a web ReadableStream via Readable.toWeb(), cancels the reader, then verifies the response can still be sent successfully.

…socket

The HTTP rewrite (#33208) ported Node.js's IncomingMessage._destroy(),
which destroys the underlying socket when the request body is not fully
consumed. However, the webstreams adapter's cancel() callback called
destroy() directly, bypassing the destroyer() helper that Node.js uses.

In Node.js, destroyer() detects server requests (via isServerRequest())
and nullifies the socket reference before calling destroy(), so _destroy()
skips socket teardown. This keeps the connection alive for the response.

Fixes #33567
The client body stream's pull() creates a setTimeout that may still be
pending when the stream is cancelled. Track and clear all pending timers
in the cancel() callback so Deno's test sanitizer does not flag a leak.

@lunadogbot lunadogbot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — verified the fix lines up with Node's lib/internal/webstreams/adapters.js, where the readable-adapter cancel callback also goes through destroyer(). Walked the chain: web-stream cancel → destroyer(req, reason)isServerRequest(req) is true (IncomingMessage has _consuming and _dumped per internal/streams/utils.js:288) → req.socket = null then req.destroy(err), so the existing _destroy body in our _http_server.js no longer tears down the connection. Independently verified that req.socket = null does not affect res.socket, since ServerResponse.assignSocket sets res.socket separately (_http_server.js:308). The regression test asserts both status 200 and body OK, which can only succeed if the socket survived the cancel; reverting line 562 of the polyfill would fail that assertion.

@bartlomieju
bartlomieju merged commit 5100f18 into main Apr 27, 2026
112 checks passed
@bartlomieju
bartlomieju deleted the fix/readable-toweb-cancel-socket branch April 27, 2026 14:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: cancelling Web stream derived from node:http IncomingMessage breaks response

2 participants