diff --git a/packages/engine.io/README.md b/packages/engine.io/README.md index f33f788f4..0aed35a7e 100644 --- a/packages/engine.io/README.md +++ b/packages/engine.io/README.md @@ -299,6 +299,28 @@ to a single process. - `net.Stream`: TCP socket for the request - `Buffer`: legacy tail bytes - **Returns** `Server` for chaining + - **Attaching custom upgrade response headers** + + When you call `Server#handleUpgrade` from your own `'upgrade'` listener + (see example (C) above), you can attach extra headers to the upgrade + response by setting `req[Symbol.for("engine.io:responseHeaders")]` to an + object before calling `handleUpgrade`. Each property is sent as an + HTTP header. Example for setting CORS headers without using the `cors` + option (which is incompatible with WebTransport): + + ```js + const httpServer = require("http").createServer(); + + 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", + }; + engine.handleUpgrade(req, socket, head); + } + }); + ``` - `attach` - Attach this Server instance to an `http.Server` - Captures `upgrade` requests for a `http.Server`. In other words, makes diff --git a/packages/engine.io/lib/server.ts b/packages/engine.io/lib/server.ts index 2d2232572..4834c06eb 100644 --- a/packages/engine.io/lib/server.ts +++ b/packages/engine.io/lib/server.ts @@ -25,7 +25,11 @@ import { objectFromEntries } from "./utils/objectFromEntries"; const debug = debugModule("engine"); -const kResponseHeaders = Symbol("responseHeaders"); +// Exposed via the runtime-shared `Symbol.for` registry so user code calling +// `Server#handleUpgrade` directly can attach additional upgrade-response +// headers (e.g. CORS headers when the `cors` middleware option is not used). +// See https://github.com/socketio/socket.io/issues/5497. +export const kResponseHeaders = Symbol.for("engine.io:responseHeaders"); type TransportName = "polling" | "websocket" | "webtransport"; @@ -675,8 +679,10 @@ class WebSocketResponse { readonly req, readonly socket: Duplex, ) { - // temporarily store the response headers on the req object (see the "headers" event) - req[kResponseHeaders] = {}; + // temporarily store the response headers on the req object (see the "headers" event). + // preserve any additional headers that the caller attached to the request before + // calling Server#handleUpgrade (see issue #5497). + req[kResponseHeaders] = { ...(req[kResponseHeaders] || {}) }; } public setHeader(name: string, value: any) { diff --git a/packages/engine.io/test/server.js b/packages/engine.io/test/server.js index bbfa86068..37640d63b 100644 --- a/packages/engine.io/test/server.js +++ b/packages/engine.io/test/server.js @@ -3684,6 +3684,57 @@ describe("server", () => { }); }); }); + + it("should forward extra upgrade-response headers set via the shared-symbol hook", function (done) { + if (process.env.EIO_WS_ENGINE === "eiows") { + return this.skip(); + } + // kResponseHeaders used to be a private Symbol unique to this module, + // so user code calling Server#handleUpgrade directly could not attach + // additional upgrade-response headers. See issue #5497. + const kResponseHeaders = Symbol.for("engine.io:responseHeaders"); + + const httpServer = http.createServer(); + const localEngine = new Server(); + let extraSeen = false; + + // Capture the engine's emitted 'headers' payload once the upgrade is + // through. We only need to confirm the extra headers arrive; the + // handshake itself is validated by the connected client below. + localEngine.on("headers", (headers) => { + if ( + headers["x-test-cors"] === "*" && + headers["x-test-extra"] === "ok" + ) { + extraSeen = true; + } + }); + + httpServer.listen(0, () => { + const port = httpServer.address().port; + + httpServer.on("upgrade", (req, socket, head) => { + if (req.url?.startsWith("/engine.io/")) { + req[kResponseHeaders] = { + "x-test-cors": "*", + "x-test-extra": "ok", + }; + return localEngine.handleUpgrade(req, socket, head); + } + }); + + client = new ClientSocket(`ws://localhost:${port}`); + + client.on("open", () => { + client.close(); + httpServer.close(() => { + expect(extraSeen).to.be(true); + done(); + }); + }); + }); + engine = localEngine; + }); }); describe("cors", () => {