Skip to content

Expose kResponseHeaders via Symbol.for for caller-provided upgrade headers - #5534

Open
MsfPablo wants to merge 1 commit into
socketio:mainfrom
MsfPablo:fix-5497
Open

Expose kResponseHeaders via Symbol.for for caller-provided upgrade headers#5534
MsfPablo wants to merge 1 commit into
socketio:mainfrom
MsfPablo:fix-5497

Conversation

@MsfPablo

Copy link
Copy Markdown

Fixes #5497 by allowing user code that calls Server#handleUpgrade directly (e.g. the README example (C) with a custom HTTP server's upgrade listener) to attach additional headers to the upgrade response, such as CORS headers when the cors option is not used.

Problem

kResponseHeaders was a private Symbol(...) allocated at module-init. Because the symbol is local to each import of engine.io, user code could not reference it from outside the module to set extra upgrade-response headers. In practice this forced callers to either (a) set the cors option on the Server — which is incompatible with WebTransport — or (b) drop direct handleUpgrade calls in favour of server.attach(httpServer), which is not always an option for projects that share the same HTTP server with other routers.

Even if the symbol were reachable, the inner WebSocketResponse class previously wiped any pre-existing req[kResponseHeaders] with req[kResponseHeaders] = {} at construction time, so caller-supplied headers would still be lost.

Fix

  1. Switch kResponseHeaders to Symbol.for("engine.io:responseHeaders") so user code can reference the well-known slot. The symbol is exported for direct introspection.
  2. In WebSocketResponse's constructor, merge any pre-existing req[kResponseHeaders] into the initial header bag rather than overwriting it.

Backwards compatibility: code that monkey-patches via the old private symbol will see undefined going forward (instead of being silently read). Code already using Symbol.for("engine.io:responseHeaders") will collide by design — that key was undocumented and not used by the project itself.

Example

httpServer.on("upgrade", (req, socket, head) => {
  if (req.url?.startsWith("/engine.io/")) {
    req[Symbol.for("engine.io:responseHeaders")] = {
      "Access-Control-Allow-Origin": req.headers.origin || "*",
      "Access-Control-Allow-Credentials": "true",
    };
    return engine.handleUpgrade(req, socket, head);
  }
});

Tests

  • Added a regression test ("should forward extra upgrade-response headers set via the shared-symbol hook") under describe('headers') that attaches two extra headers via the shared symbol, opens a ws handshake, and asserts both are emitted through 'headers'.
  • The existing "headers" test suite continues to pass unchanged.
  • npm run format:check is green; build/compile is green.

🤖 Generated with Claude Code

When user code calls Server#handleUpgrade from its own 'upgrade' listener
(such as the README example (C) with a manual http.Server), the underlying
WebSocketResponse would overwrite any response headers previously attached
to the request with an empty object, making it impossible to set CORS (or
any other) headers on the upgrade response without using the .cors option.

Switch kResponseHeaders to Symbol.for("engine.io:responseHeaders") so it
is reachable from user code, and merge any pre-existing header bag into
the WebSocketResponse's initial headers instead of starting from {}.

Closes socketio#5497
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.

Can't set response headers in Server.handleUpgrade

1 participant