Skip to content

fix(forward): destroy outbound socket when client disconnects early - #671

Open
bliuchak wants to merge 2 commits into
masterfrom
fix/forward-socker-leak
Open

fix(forward): destroy outbound socket when client disconnects early#671
bliuchak wants to merge 2 commits into
masterfrom
fix/forward-socker-leak

Conversation

@bliuchak

Copy link
Copy Markdown
Contributor

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

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
@bliuchak bliuchak self-assigned this Jul 27, 2026
Copilot AI review requested due to automatic review settings July 27, 2026 20:28
@bliuchak bliuchak added adhoc Ad-hoc unplanned task added during the sprint. t-unblocking Issues with this label are in the ownership of the unblocking team. labels Jul 27, 2026
@github-actions github-actions Bot added the tested Temporary label used only programatically for some analytics. label Jul 27, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.ClientRequest when the downstream ServerResponse closes in forward.ts and forward_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.headersSent may 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 === false and 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.

Comment thread test/e2e/forward-socket-cleanup.js Outdated
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.
Copilot AI review requested due to automatic review settings July 28, 2026 09:04

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread src/forward.ts
Comment thread src/forward_socks.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

adhoc Ad-hoc unplanned task added during the sprint. t-unblocking Issues with this label are in the ownership of the unblocking team. tested Temporary label used only programatically for some analytics.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

forward.ts: Socket leak when client.on('error') fires — missing client.destroy()

5 participants