Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Commit 92d98c9

Browse files
committed
Include headers from WebSocket handshake in Responses, closes #151
Headers included in `new Response`s to WebSocket upgrade requests are now returned to the client when using the HTTP server. Headers received in the upgrade response when using `fetch` as a WebSocket client are now included in returned `Response`.
1 parent 78bc418 commit 92d98c9

File tree

6 files changed

+115
-21
lines changed

6 files changed

+115
-21
lines changed

packages/core/src/standards/http.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,31 @@ Headers.prototype.getAll = function (key: string): string[] {
120120
return value ? splitCookiesString(value) : [];
121121
};
122122

123+
/** @internal */
124+
export function _headersFromIncomingRequest(
125+
req: http.IncomingMessage
126+
): Headers {
127+
const headers = new Headers();
128+
for (const [name, values] of Object.entries(req.headers)) {
129+
// These headers are unsupported in undici fetch requests, they're added
130+
// automatically
131+
if (
132+
name === "transfer-encoding" ||
133+
name === "connection" ||
134+
name === "keep-alive" ||
135+
name === "expect"
136+
) {
137+
continue;
138+
}
139+
if (Array.isArray(values)) {
140+
for (const value of values) headers.append(name, value);
141+
} else if (values !== undefined) {
142+
headers.append(name, values);
143+
}
144+
}
145+
return headers;
146+
}
147+
123148
// Instead of subclassing our customised Request and Response classes from
124149
// BaseRequest and BaseResponse, we instead compose them and implement the same
125150
// interface.

packages/core/src/standards/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from "./domexception";
44
export * from "./encoding";
55
export * from "./event";
66
export {
7+
_headersFromIncomingRequest,
78
_kInner,
89
_isByteStream,
910
Body,

packages/http-server/src/index.ts

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// noinspection ES6ConvertVarToLetConst
22

33
import assert from "assert";
4-
import http, { OutgoingHttpHeaders } from "http";
4+
import http from "http";
55
import https from "https";
66
import { Transform, Writable } from "stream";
77
import { ReadableStream } from "stream/web";
@@ -13,6 +13,7 @@ import {
1313
Request,
1414
Response,
1515
_getBodyLength,
16+
_headersFromIncomingRequest,
1617
logResponse,
1718
} from "@miniflare/core";
1819
import { prefixError, randomHex } from "@miniflare/shared";
@@ -105,24 +106,7 @@ export async function convertNodeRequest(
105106
req.headers["host"] = url.host;
106107

107108
// Build Headers object from request
108-
const headers = new Headers();
109-
for (const [name, values] of Object.entries(req.headers)) {
110-
// These headers are unsupported in undici fetch requests, they're added
111-
// automatically
112-
if (
113-
name === "transfer-encoding" ||
114-
name === "connection" ||
115-
name === "keep-alive" ||
116-
name === "expect"
117-
) {
118-
continue;
119-
}
120-
if (Array.isArray(values)) {
121-
for (const value of values) headers.append(name, value);
122-
} else if (values !== undefined) {
123-
headers.append(name, values);
124-
}
125-
}
109+
const headers = _headersFromIncomingRequest(req);
126110

127111
// Create Request with additional Cloudflare specific properties:
128112
// https://developers.cloudflare.com/workers/runtime-apis/request#incomingrequestcfproperties
@@ -183,7 +167,7 @@ export function createRequestListener<Plugins extends HTTPPluginSignatures>(
183167
response = await mf.dispatchFetch(request);
184168
waitUntil = response.waitUntil();
185169
status = response.status;
186-
const headers: OutgoingHttpHeaders = {};
170+
const headers: http.OutgoingHttpHeaders = {};
187171
// eslint-disable-next-line prefer-const
188172
for (let [key, value] of response.headers) {
189173
key = key.toLowerCase();
@@ -334,6 +318,12 @@ export function createRequestListener<Plugins extends HTTPPluginSignatures>(
334318
};
335319
}
336320

321+
const restrictedWebSocketUpgradeHeaders = [
322+
"upgrade",
323+
"connection",
324+
"sec-websocket-accept",
325+
];
326+
337327
export async function createServer<Plugins extends HTTPPluginSignatures>(
338328
mf: MiniflareCore<Plugins>,
339329
options?: http.ServerOptions & https.ServerOptions
@@ -356,6 +346,21 @@ export async function createServer<Plugins extends HTTPPluginSignatures>(
356346
// Setup WebSocket servers
357347
const webSocketServer = new WebSocketServer({ noServer: true });
358348
const liveReloadServer = new WebSocketServer({ noServer: true });
349+
350+
// Add custom headers included in response to WebSocket upgrade requests
351+
const extraHeaders = new WeakMap<http.IncomingMessage, Headers>();
352+
webSocketServer.on("headers", (headers, req) => {
353+
const extra = extraHeaders.get(req);
354+
extraHeaders.delete(req);
355+
if (extra) {
356+
for (const [key, value] of extra) {
357+
if (!restrictedWebSocketUpgradeHeaders.includes(key)) {
358+
headers.push(`${key}: ${value}`);
359+
}
360+
}
361+
}
362+
});
363+
359364
server.on("upgrade", async (request, socket, head) => {
360365
// Only interested in pathname so base URL doesn't matter
361366
const { pathname } = new URL(request.url ?? "", "http://localhost");
@@ -382,6 +387,7 @@ export async function createServer<Plugins extends HTTPPluginSignatures>(
382387
}
383388

384389
// Accept and couple the Web Socket
390+
extraHeaders.set(request, response.headers);
385391
webSocketServer.handleUpgrade(request, socket as any, head, (ws) => {
386392
void coupleWebSocket(ws, webSocket);
387393
webSocketServer.emit("connection", ws, request);

packages/http-server/test/index.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,29 @@ test("createServer: handles web socket upgrades", async (t) => {
598598
});
599599
t.is(await eventPromise, "worker:hello");
600600
});
601+
test("createServer: includes headers from web socket upgrade response", async (t) => {
602+
// https://github.com/cloudflare/miniflare/issues/151
603+
const mf = useMiniflareWithHandler(
604+
{ HTTPPlugin, WebSocketPlugin },
605+
{},
606+
async (globals) => {
607+
const [client, worker] = Object.values(new globals.WebSocketPair());
608+
worker.accept();
609+
return new globals.Response(null, {
610+
status: 101,
611+
webSocket: client,
612+
headers: { "Set-Cookie": "key=value" },
613+
});
614+
}
615+
);
616+
const port = await listen(t, await createServer(mf));
617+
618+
const ws = new StandardWebSocket(`ws://localhost:${port}`);
619+
const [trigger, promise] = triggerPromise<http.IncomingMessage>();
620+
ws.addListener("upgrade", (req) => trigger(req));
621+
const req = await promise;
622+
t.deepEqual(req.headers["set-cookie"], ["key=value"]);
623+
});
601624
test("createServer: expects status 101 and web socket response for upgrades", async (t) => {
602625
const log = new TestLog();
603626
log.error = (message) => log.logWithLevel(LogLevel.ERROR, message.toString());

packages/web-sockets/src/fetch.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import {
44
RequestInfo,
55
RequestInit,
66
Response,
7+
_headersFromIncomingRequest,
78
fetch,
89
} from "@miniflare/core";
910
import { getRequestContext } from "@miniflare/shared";
11+
import { Headers } from "undici";
1012
import StandardWebSocket from "ws";
1113
import { coupleWebSocket } from "./couple";
1214
import { WebSocketPair } from "./websocket";
@@ -55,12 +57,22 @@ export async function upgradingFetch(
5557
headers,
5658
});
5759

60+
// Get response headers from upgrade
61+
let headersResolve: (headers: Headers) => void;
62+
const headersPromise = new Promise<Headers>((resolve) => {
63+
headersResolve = resolve;
64+
});
65+
ws.once("upgrade", (req) => {
66+
headersResolve(_headersFromIncomingRequest(req));
67+
});
68+
5869
// Couple web socket with pair and resolve
5970
const [worker, client] = Object.values(new WebSocketPair());
6071
await coupleWebSocket(ws, client);
6172
return new Response(null, {
6273
status: 101,
6374
webSocket: worker,
75+
headers: await headersPromise,
6476
});
6577
}
6678

packages/web-sockets/test/fetch.spec.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import assert from "assert";
22
import { Blob } from "buffer";
3+
import http from "http";
4+
import { AddressInfo } from "net";
35
import { TransformStream } from "stream/web";
46
import { URLSearchParams } from "url";
57
import { CachePlugin } from "@miniflare/cache";
@@ -25,7 +27,10 @@ import {
2527
} from "@miniflare/web-sockets";
2628
import test from "ava";
2729
import { FormData } from "undici";
28-
import StandardWebSocket, { MessageEvent as WebSocketMessageEvent } from "ws";
30+
import StandardWebSocket, {
31+
MessageEvent as WebSocketMessageEvent,
32+
WebSocketServer,
33+
} from "ws";
2934

3035
test("upgradingFetch: performs regular http request", async (t) => {
3136
const upstream = (await useServer(t, (req, res) => res.end("upstream"))).http;
@@ -98,6 +103,28 @@ test("upgradingFetch: performs web socket upgrade with Sec-WebSocket-Protocol he
98103
const event = await eventPromise;
99104
t.is(event.data, "protocol1,proto2,p3");
100105
});
106+
test("upgradingFetch: includes headers from web socket upgrade response", async (t) => {
107+
const server = http.createServer();
108+
const wss = new WebSocketServer({ server });
109+
wss.on("connection", (ws) => {
110+
ws.send("hello");
111+
ws.close();
112+
});
113+
wss.on("headers", (headers) => {
114+
headers.push("Set-Cookie: key=value");
115+
});
116+
const port = await new Promise<number>((resolve) => {
117+
server.listen(0, () => {
118+
t.teardown(() => server.close());
119+
resolve((server.address() as AddressInfo).port);
120+
});
121+
});
122+
const res = await upgradingFetch(`http://localhost:${port}`, {
123+
headers: { upgrade: "websocket" },
124+
});
125+
t.not(res.webSocket, undefined);
126+
t.is(res.headers.get("set-cookie"), "key=value");
127+
});
101128
test("upgradingFetch: throws on ws(s) protocols", async (t) => {
102129
await t.throwsAsync(
103130
upgradingFetch("ws://localhost/", {

0 commit comments

Comments
 (0)