fix(forward): destroy outbound socket when client disconnects early - #671
Open
bliuchak wants to merge 2 commits into
Open
fix(forward): destroy outbound socket when client disconnects early#671bliuchak wants to merge 2 commits into
bliuchak wants to merge 2 commits into
Conversation
forward.ts (and forward_socks.ts) never destroyed the outbound request
when the client-facing response closed first while the upstream target
had accepted the connection but never responded - e.g. during
server.close(true) shutdown, or the browser disconnecting. chain.ts
already had this symmetry (sourceSocket.on('close', () => targetSocket
.destroy())); forward.ts/forward_socks.ts didn't, leaving the outbound
socket open indefinitely.
The originally reported diagnosis (missing client.destroy() in
client.on('error')) turned out to be a no-op - Node already destroys
the socket there internally - so the fix targets the actual gap
instead.
Fixes #670
There was a problem hiding this comment.
Pull request overview
Fixes an outbound socket leak in the HTTP forwarders by ensuring upstream requests are torn down when the client-facing side closes before the upstream responds (e.g., during server.close(true) shutdown or early client disconnects).
Changes:
- Destroy upstream
http.ClientRequestwhen the downstreamServerResponsecloses inforward.tsandforward_socks.ts. - Add an e2e regression test that reproduces the “upstream accepts but never responds” socket-leak scenario.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
src/forward.ts |
Adds client-response close handling to destroy the upstream request/socket to prevent hangs/leaks. |
src/forward_socks.ts |
Mirrors the same upstream-destroy-on-downstream-close behavior for SOCKS-forwarded HTTP. |
test/e2e/forward-socket-cleanup.js |
New e2e test validating the outbound socket is destroyed when the client side disappears mid-request. |
Comments suppressed due to low confidence (2)
src/forward.ts:154
- After adding the response 'close' handler that can destroy the upstream request, the upstream ClientRequest can emit 'error' after the client-facing response has already been closed/destroyed. In that case
response.headersSentmay still be false, and the handler below will try to set headers/end on a destroyed ServerResponse, which can throw (e.g. ERR_STREAM_DESTROYED / write after end) and potentially crash the process. Guard the error handler against already-closed responses (destroyed/writableEnded) before attempting to write an error response.
client.on('error', (error: NodeJS.ErrnoException) => {
if (response.headersSent) {
resolve();
return;
}
src/forward_socks.ts:107
- Same pattern as forward.ts: if the client-facing response is already closed/destroyed (including cases where the new response 'close' handler destroys the upstream request), the ClientRequest 'error' handler can still run with
headersSent === falseand then attempts to write headers/body to a destroyed ServerResponse. Add a destroyed/writableEnded guard before writing the error response to avoid ERR_STREAM_DESTROYED/write-after-end failures.
client.on('error', (error: NodeJS.ErrnoException) => {
if (response.headersSent) {
resolve();
return;
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The polling wait for the outbound socket to exist had no rejection path, so a regression that stopped httpAgent.createConnection() from being called would surface as a generic mocha timeout instead of a clear failure message. Address Copilot's review comment on #671.
jirimoravcik
approved these changes
Jul 28, 2026
daniil-poletaev
approved these changes
Jul 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
forward.ts (and forward_socks.ts) never destroyed the outbound request when the client-facing response closed first while the upstream target had accepted the connection but never responded - e.g. during server.close(true) shutdown, or the browser disconnecting. chain.ts already had this symmetry (sourceSocket.on('close', () => targetSocket .destroy())); forward.ts/forward_socks.ts didn't, leaving the outbound socket open indefinitely.
The originally reported diagnosis (missing client.destroy() in client.on('error')) turned out to be a no-op - Node already destroys the socket there internally - so the fix targets the actual gap instead.
Fixes #670