feat(unstable): WebSocket headers field (#30321)

This changes the second argument in the WebSocket constructor to be able
to take an object, which can contain a headers field with which the
headers for the connection can be set.

---------

Co-authored-by: Luca Casonato <hello@lcas.dev>
This commit is contained in:
Leo Kettmeir 2025-08-28 11:44:59 +02:00 committed by GitHub
parent b88c621f22
commit c217928649
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 148 additions and 23 deletions

View file

@ -267,6 +267,13 @@ interface WebSocket extends EventTarget {
* // Using URL object instead of string * // Using URL object instead of string
* const url = new URL("ws://localhost:8080/path"); * const url = new URL("ws://localhost:8080/path");
* const wsWithUrl = new WebSocket(url); * const wsWithUrl = new WebSocket(url);
*
* // WebSocket with headers
* const wsWithProtocols = new WebSocket("ws://localhost:8080", {
* headers: {
* "Authorization": "Bearer foo",
* },
* });
* ``` * ```
* *
* @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket * @see https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket
@ -274,13 +281,34 @@ interface WebSocket extends EventTarget {
*/ */
declare var WebSocket: { declare var WebSocket: {
readonly prototype: WebSocket; readonly prototype: WebSocket;
new (url: string | URL, protocols?: string | string[]): WebSocket; new (
url: string | URL,
protocolsOrOptions?: string | string[] | WebSocketOptions,
): WebSocket;
readonly CLOSED: number; readonly CLOSED: number;
readonly CLOSING: number; readonly CLOSING: number;
readonly CONNECTING: number; readonly CONNECTING: number;
readonly OPEN: number; readonly OPEN: number;
}; };
/**
* Options for a WebSocket instance.
* This feature is non-standard.
*
* @category WebSockets
*/
interface WebSocketOptions {
/**
* The sub-protocol(s) that the client would like to use, in order of preference.
*/
protocols?: string | string[];
/**
* A Headers object, an object literal, or an array of two-item arrays to set handshake's headers.
* This feature is non-standard.
*/
headers?: HeadersInit;
}
/** /**
* Specifies the type of binary data being received over a `WebSocket` connection. * Specifies the type of binary data being received over a `WebSocket` connection.
* *

View file

@ -6,8 +6,6 @@ import { primordials } from "ext:core/mod.js";
import { op_utf8_to_byte_string } from "ext:core/ops"; import { op_utf8_to_byte_string } from "ext:core/ops";
const { const {
ArrayPrototypeFind, ArrayPrototypeFind,
ArrayPrototypeSlice,
ArrayPrototypeSplice,
Number, Number,
NumberIsFinite, NumberIsFinite,
NumberIsNaN, NumberIsNaN,
@ -200,16 +198,7 @@ class EventSource extends EventTarget {
); );
if (this.#headers) { if (this.#headers) {
const headerList = headerListFromHeaders(initialHeaders); fillHeaders(initialHeaders, this.#headers);
const headers = this.#headers ?? ArrayPrototypeSlice(
headerList,
0,
headerList.length,
);
if (headerList.length !== 0) {
ArrayPrototypeSplice(headerList, 0, headerList.length);
}
fillHeaders(initialHeaders, headers);
} }
const req = newInnerRequest( const req = newInnerRequest(

View file

@ -23,6 +23,7 @@ import {
} from "ext:core/ops"; } from "ext:core/ops";
const { const {
ArrayBufferIsView, ArrayBufferIsView,
ArrayIsArray,
ArrayPrototypeJoin, ArrayPrototypeJoin,
ArrayPrototypeMap, ArrayPrototypeMap,
ArrayPrototypePush, ArrayPrototypePush,
@ -64,18 +65,41 @@ import {
} from "ext:deno_web/02_event.js"; } from "ext:deno_web/02_event.js";
import { Blob, BlobPrototype } from "ext:deno_web/09_file.js"; import { Blob, BlobPrototype } from "ext:deno_web/09_file.js";
import { getLocationHref } from "ext:deno_web/12_location.js"; import { getLocationHref } from "ext:deno_web/12_location.js";
import {
fillHeaders,
headerListFromHeaders,
headersFromHeaderList,
} from "ext:deno_fetch/20_headers.js";
webidl.converters["sequence<DOMString> or DOMString"] = ( webidl.converters["WebSocketInit"] = webidl.createDictionaryConverter(
"WebSocketInit",
[
{
key: "headers",
converter: webidl.converters["HeadersInit"],
},
{
key: "protocols",
converter: webidl.converters["sequence<DOMString>"],
},
],
);
webidl.converters["WebSocketInit or sequence<DOMString> or DOMString"] = (
V, V,
prefix, prefix,
context, context,
opts, opts,
) => { ) => {
// Union for (sequence<DOMString> or DOMString) // Union for (WebSocketInit or sequence<DOMString> or DOMString)
if (V === null || V === undefined) {
return webidl.converters["WebSocketInit"](V, prefix, context, opts);
}
if (webidl.type(V) === "Object" && V !== null) { if (webidl.type(V) === "Object" && V !== null) {
if (V[SymbolIterator] !== undefined) { if (V[SymbolIterator] !== undefined) {
return webidl.converters["sequence<DOMString>"](V, prefix, context, opts); return webidl.converters["sequence<DOMString>"](V, prefix, context, opts);
} }
return webidl.converters["WebSocketInit"](V, prefix, context, opts);
} }
return webidl.converters.DOMString(V, prefix, context, opts); return webidl.converters.DOMString(V, prefix, context, opts);
}; };
@ -124,7 +148,7 @@ const _idleTimeoutTimeout = Symbol("[[idleTimeoutTimeout]]");
const _serverHandleIdleTimeout = Symbol("[[serverHandleIdleTimeout]]"); const _serverHandleIdleTimeout = Symbol("[[serverHandleIdleTimeout]]");
class WebSocket extends EventTarget { class WebSocket extends EventTarget {
constructor(url, protocols = []) { constructor(url, initOrProtocols) {
super(); super();
this[webidl.brand] = webidl.brand; this[webidl.brand] = webidl.brand;
this[_rid] = undefined; this[_rid] = undefined;
@ -142,11 +166,12 @@ class WebSocket extends EventTarget {
const prefix = "Failed to construct 'WebSocket'"; const prefix = "Failed to construct 'WebSocket'";
webidl.requiredArguments(arguments.length, 1, prefix); webidl.requiredArguments(arguments.length, 1, prefix);
url = webidl.converters.USVString(url, prefix, "Argument 1"); url = webidl.converters.USVString(url, prefix, "Argument 1");
protocols = webidl.converters["sequence<DOMString> or DOMString"]( initOrProtocols = webidl.converters
protocols, ["WebSocketInit or sequence<DOMString> or DOMString"](
prefix, initOrProtocols,
"Argument 2", prefix,
); "Argument 2",
);
let wsURL; let wsURL;
@ -179,8 +204,20 @@ class WebSocket extends EventTarget {
this[_url] = wsURL.href; this[_url] = wsURL.href;
this[_role] = CLIENT; this[_role] = CLIENT;
if (typeof protocols === "string") { let protocols;
protocols = [protocols]; let headers = null;
if (typeof initOrProtocols === "string") {
protocols = [initOrProtocols];
} else if (ArrayIsArray(initOrProtocols)) {
protocols = initOrProtocols;
} else {
protocols = initOrProtocols.protocols || [];
if (initOrProtocols.headers !== undefined) {
headers = headersFromHeaderList([], "request");
fillHeaders(headers, initOrProtocols.headers);
}
} }
if ( if (
@ -224,6 +261,7 @@ class WebSocket extends EventTarget {
wsURL.href, wsURL.href,
ArrayPrototypeJoin(protocols, ", "), ArrayPrototypeJoin(protocols, ", "),
cancelRid, cancelRid,
headers ? headerListFromHeaders(headers) : null,
), ),
(create) => { (create) => {
this[_rid] = create.rid; this[_rid] = create.rid;

View file

@ -1,4 +1,7 @@
// Copyright 2018-2025 the Deno authors. MIT license. // Copyright 2018-2025 the Deno authors. MIT license.
// deno-lint-ignore-file no-console
import { import {
assert, assert,
assertEquals, assertEquals,
@ -867,3 +870,68 @@ Deno.test("websocket close ongoing handshake", async () => {
assert(gotError2); assert(gotError2);
} }
}); });
function createOnErrorCb(ac: AbortController): (err: unknown) => Response {
return (err) => {
console.error(err);
ac.abort();
return new Response("Internal server error", { status: 500 });
};
}
function onListen(
resolve: (value: void | PromiseLike<void>) => void,
): ({ hostname, port }: { hostname: string; port: number }) => void {
return () => {
resolve();
};
}
Deno.test("WebSocket headers", async () => {
const ac = new AbortController();
const listeningDeferred = Promise.withResolvers<void>();
const doneDeferred = Promise.withResolvers<void>();
await using server = Deno.serve({
handler: (request) => {
assertEquals(request.headers.get("Authorization"), "Bearer foo");
const {
response,
socket,
} = Deno.upgradeWebSocket(request);
socket.onerror = (e) => {
console.error(e);
fail();
};
socket.onmessage = (m) => {
socket.send(m.data);
socket.close(1001);
};
socket.onclose = () => doneDeferred.resolve();
return response;
},
port: servePort,
signal: ac.signal,
onListen: onListen(listeningDeferred.resolve),
onError: createOnErrorCb(ac),
});
await listeningDeferred.promise;
const def = Promise.withResolvers<void>();
const ws = new WebSocket(`ws://localhost:${servePort}`, {
headers: {
"Authorization": "Bearer foo",
},
});
ws.onmessage = (m) => assertEquals(m.data, "foo");
ws.onerror = (e) => {
console.error(e);
fail();
};
ws.onclose = () => def.resolve();
ws.onopen = () => ws.send("foo");
await def.promise;
await doneDeferred.promise;
ac.abort();
await server.finished;
});

View file

@ -14554,6 +14554,7 @@
"eventhandlers.any.worker.html?wpt_flags=h2": true, "eventhandlers.any.worker.html?wpt_flags=h2": true,
"eventhandlers.any.worker.html?wss": true, "eventhandlers.any.worker.html?wss": true,
"idlharness.any.html": [ "idlharness.any.html": [
"WebSocket interface object length",
"WebSocket interface: constant CONNECTING on interface object", "WebSocket interface: constant CONNECTING on interface object",
"WebSocket interface: constant CONNECTING on interface prototype object", "WebSocket interface: constant CONNECTING on interface prototype object",
"WebSocket interface: constant OPEN on interface object", "WebSocket interface: constant OPEN on interface object",
@ -14572,6 +14573,7 @@
"Stringification of new CloseEvent(\"close\")" "Stringification of new CloseEvent(\"close\")"
], ],
"idlharness.any.worker.html": [ "idlharness.any.worker.html": [
"WebSocket interface object length",
"WebSocket interface: constant CONNECTING on interface object", "WebSocket interface: constant CONNECTING on interface object",
"WebSocket interface: constant CONNECTING on interface prototype object", "WebSocket interface: constant CONNECTING on interface prototype object",
"WebSocket interface: constant OPEN on interface object", "WebSocket interface: constant OPEN on interface object",