Skip to content

fix(eio): make the upgrade response an EventEmitter - #5563

Open
Minhal128 wants to merge 1 commit into
socketio:mainfrom
Minhal128:fix/ws-response-event-emitter
Open

Minhal128 wants to merge 1 commit into
socketio:mainfrom
Minhal128:fix/ws-response-event-emitter

Conversation

@Minhal128

Copy link
Copy Markdown

Fixes #5072

The problem

WebSocketResponse (packages/engine.io/lib/server.ts) exists so that Express-style middlewares can be applied to an upgrade request. It documents itself as exposing "a subset of the http.ServerResponse interface", but it was a plain class with only setHeader / getHeader / removeHeader / write / writeHead / end.

Any middleware that observes the response lifecycle blows up on it. pino-http — and most other request loggers — do exactly that:

res.on('close', onResponseComplete)
    ^
TypeError: res.on is not a function

Since this happens inside _applyMiddlewares during handleUpgrade, it doesn't just skip the log — it throws out of the upgrade, so the WebSocket connection never establishes.

The change

  • WebSocketResponse extends EventEmitter, so res.on(...) works like it does for a real ServerResponse.
  • "finish" and "close" are emitted once ws.handleUpgrade has written the handshake, so a response logger actually completes rather than registering a listener that never fires. That is what the reporter was after — "I expected the upgrade request to be logged at upgrade time".
  • statusCode is set to 101, otherwise a logger reports the handshake with an undefined status. Happy to drop this if you'd rather keep the shim minimal.

Scope — uWebSockets.js is not covered

userver.ts has its own ResponseWrapper, which has the same gap. I left it alone because its response lifecycle (polling vs res.upgrade()) is a separate question and I'd rather not guess at the right emit points. The new test therefore skips under EIO_WS_ENGINE=uws, the same way should end the request (polling) already does in that file. Say the word and I'll extend it in this PR or a follow-up.

Verification

Ran the engine.io suite on a fork:

branch result
test only, without the fix ❌ Uncaught TypeError: res.on is not a function — the exact error from the issue
test + fix ✅ green
unmodified main (baseline) ✅ green

One note for transparency: the first run of the fixed branch failed on WebTransport › should close a connection that sends an invalid upgrade (bis) with a 2000 ms timeout. It passed on re-run and is green on the baseline too, so it looks flaky rather than related — but flagging it in case it's a known one.

WebSocketResponse exposes a subset of http.ServerResponse so that Express
middlewares can be applied to an upgrade request, but it was a plain
object. Any middleware that observes the response lifecycle — pino-http
and most other request loggers do, via res.on('close') — threw
"TypeError: res.on is not a function" and killed the upgrade.

Extend EventEmitter and emit "finish" then "close" once the handshake has
been written, so those middlewares run to completion instead of crashing.
statusCode is set to 101 so a logger reports the handshake rather than an
undefined status.
Copilot AI lite review requested due to automatic review settings September 24, 2026 14:45

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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

2 participants