Skip to content

Register the connection listener explicitly, so re-emitted sockets are handled - #4

Closed
gkurt wants to merge 1 commit into
httptoolkit:mainfrom
gkurt:explicit-connection-listener
Closed

Register the connection listener explicitly, so re-emitted sockets are handled#4
gkurt wants to merge 1 commit into
httptoolkit:mainfrom
gkurt:explicit-connection-listener

Conversation

@gkurt

@gkurt gkurt commented Aug 31, 2026

Copy link
Copy Markdown

Server currently passes its connection handler to net.Server's constructor:

super((socket) => this.connectionListener(socket));

In Node this is exactly equivalent to registering a 'connection' listener — the
constructor argument is added with this.on('connection', …) — so re-emitting a
socket onto the server dispatches to it. That behaviour is load-bearing for
httpolyglot's main consumer: mockttp re-injects the tunnelled socket after CONNECT
with server.emit('connection', socket), so that a tunnelled connection gets sniffed
and routed exactly like a direct one.

The equivalence doesn't hold on every runtime. Bun handles the constructor callback
internally rather than registering it as a listener: listeners('connection') is empty,
and emit('connection', socket) never reaches connectionListener. Genuine inbound
connections still work, so a polyglot server looks healthy right up until something
re-injects a socket — at which point the socket is silently never read and the peer
hangs with no error on either side. (Filed upstream at oven-sh/bun#41060.)

This switches to the explicit form:

super();
this.on('connection', (socket) => this.connectionListener(socket));

which is a no-op on Node — same registration, same ordering, nothing can attach a
listener between super() and the .on() call — and makes the re-injection contract
hold on runtimes that treat the constructor argument specially.

Tests

Adds test/reinjected-connection.spec.ts, which stands up a plain net.Server in the
role of a proxy's CONNECT handler, hands the accepted socket back to the polyglot
server via emit('connection', …), and asserts the request is routed to the HTTP
listener.

before after
full suite, Node 24.20.0 19 passing 20 passing
new test, Node 24.20.0 passes passes
new test, Bun 1.4.0 fails (2s timeout) passes

The new test passes on Node either way — it can't fail there, since the two forms are
equivalent. Its value is that it pins a contract that mockttp depends on and that is
currently only implicit.

One note if you reproduce the Bun column: bun x mocha runs mocha under Node (the
binary's shebang wins), so the test appears to pass unpatched. bun --bun x mocha is
what actually runs it under Bun.

Notes

I'm not asking you to support Bun — the ALPN gap below means httpolyglot's TLS path
can't fully work there yet regardless. This is narrower than that: the constructor
form's equivalence to .on('connection') is a Node implementation detail, and the
explicit form costs nothing to prefer.

The two forms are equivalent in Node, where net.Server's constructor argument
is registered as a 'connection' listener. Only the explicit form is portable:
runtimes that handle the constructor callback internally never dispatch to it
from server.emit('connection', socket), which is how a proxy re-injects a
tunnelled socket after CONNECT.

Adds a test covering that re-injection path, which mockttp depends on and
which was previously only implicit.
@gkurt
gkurt force-pushed the explicit-connection-listener branch from 0df4487 to d36c4b9 Compare August 31, 2026 16:15
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@gkurt

gkurt commented Aug 31, 2026

Copy link
Copy Markdown
Author

@pimterry sorry for the PR. I intended to review this before opening a PR, but Claude opened it anyway. This indeed needs to be fixed in Bun, and seems like it's already fixed on main. I am so sorry for this. Closing.

@gkurt gkurt closed this Aug 31, 2026
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.

2 participants