Skip to content

fix(eio): expose EventEmitter interface on WebSocket upgrade response - #5526

Open
nabsei wants to merge 2 commits into
socketio:mainfrom
nabsei:fix/eio-websocket-response-eventemitter
Open

fix(eio): expose EventEmitter interface on WebSocket upgrade response#5526
nabsei wants to merge 2 commits into
socketio:mainfrom
nabsei:fix/eio-websocket-response-eventemitter

Conversation

@nabsei

@nabsei nabsei commented Jul 14, 2026

Copy link
Copy Markdown

The kind of change this PR does introduce

  • a bug fix
  • a new feature
  • an update to the documentation
  • a code change that improves performance
  • other

Current behavior

Fixes #5072. During a WebSocket upgrade, engine.io applies middlewares against a WebSocketResponse object that only implements a small subset of http.ServerResponse (setHeader/getHeader/removeHeader/write/writeHead/end). It does not implement EventEmitter, so any middleware that calls res.on(...), for example pino-http, which listens for "close" to know when the request finished, crashes with TypeError: res.on is not a function.

New behavior

WebSocketResponse now extends EventEmitter and emits "close" when the underlying socket closes, and "finish" when end() is called, mirroring the events a real http.ServerResponse emits. Added a regression test in test/middlewares.js that registers a res.on("close", ...) listener during a WebSocket upgrade and asserts it fires.

Other information (e.g. related issues)

Parts of this fix (root-cause analysis and implementation) were produced with the help of Claude Code. I reviewed and understood the change before opening this PR.

Verification performed:

  • Added unit test passes, along with the full existing middlewares.js/server.js/engine.io.js suite (175 passing, 2 pre-existing unrelated pending), no regressions.
  • Reproduced the exact bug report with the real pino-http package against an unpatched build (crashes with res.on is not a function), then confirmed the same script completes cleanly with this fix applied.
  • prettier --check passes on the changed files.

@nabsei

nabsei commented Jul 19, 2026

Copy link
Copy Markdown
Author

Just checking in on this one — happy to make any changes if something's needed, or let me know if it's not a good fit. No rush either way.

nabsei added 2 commits July 27, 2026 14:15
WebSocketResponse (the fake response object passed to middlewares
during a WebSocket upgrade) did not implement EventEmitter, so any
middleware calling res.on(...), e.g. pino-http listening for "close"
to know when the request finished, crashed with "res.on is not a
function".

Make WebSocketResponse extend EventEmitter and emit "close" when the
underlying socket closes, and "finish" when end() is called, mirroring
the events a real http.ServerResponse emits.

Fixes socketio#5072
The previous commit only patched WebSocketResponse in server.ts (used by
the plain Node.js HTTP server). userver.ts (used with EIO_WS_ENGINE=uws)
has its own separate response class, ResponseWrapper, which didn't get
the same fix, so the pino-http regression test failed specifically
under the uws engine.

Two changes were needed, not just extending EventEmitter: unlike the
Node.js HTTP server, where the same TCP socket persists across a
WebSocket upgrade (so listening for its "close" event works directly),
uWebSockets.js invalidates the HTTP response handle once res.upgrade()
succeeds, and res.onAborted() never fires afterwards. The ResponseWrapper
now also gets threaded through the WebSocket's user data on upgrade, so
it can be told to emit "close" from the `close` handler on the `.ws()`
route once the actual WebSocket connection ends.

Verified: full engine.io suite passes under both EIO_WS_ENGINE=uws
(178 passing, 0 failing, up from 177/1) and the default ws engine
(175 passing on server.js/engine.io.js/middlewares.js, no regressions).
prettier --check passes.
@nabsei
nabsei force-pushed the fix/eio-websocket-response-eventemitter branch from 10c0eed to facee04 Compare July 27, 2026 12:17
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.

Log with pino-http middleware raises TypeError: res.on is not a function

1 participant