diff --git a/cli/tsc/dts/lib.deno_websocket.d.ts b/cli/tsc/dts/lib.deno_websocket.d.ts index 443bc06b3c..3ca343c9fd 100644 --- a/cli/tsc/dts/lib.deno_websocket.d.ts +++ b/cli/tsc/dts/lib.deno_websocket.d.ts @@ -267,6 +267,13 @@ interface WebSocket extends EventTarget { * // Using URL object instead of string * const url = new URL("ws://localhost:8080/path"); * 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 @@ -274,13 +281,34 @@ interface WebSocket extends EventTarget { */ declare var 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 CLOSING: number; readonly CONNECTING: 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. * diff --git a/ext/fetch/27_eventsource.js b/ext/fetch/27_eventsource.js index c82a4455e5..4affd2caa2 100644 --- a/ext/fetch/27_eventsource.js +++ b/ext/fetch/27_eventsource.js @@ -6,8 +6,6 @@ import { primordials } from "ext:core/mod.js"; import { op_utf8_to_byte_string } from "ext:core/ops"; const { ArrayPrototypeFind, - ArrayPrototypeSlice, - ArrayPrototypeSplice, Number, NumberIsFinite, NumberIsNaN, @@ -200,16 +198,7 @@ class EventSource extends EventTarget { ); if (this.#headers) { - const headerList = headerListFromHeaders(initialHeaders); - const headers = this.#headers ?? ArrayPrototypeSlice( - headerList, - 0, - headerList.length, - ); - if (headerList.length !== 0) { - ArrayPrototypeSplice(headerList, 0, headerList.length); - } - fillHeaders(initialHeaders, headers); + fillHeaders(initialHeaders, this.#headers); } const req = newInnerRequest( diff --git a/ext/websocket/01_websocket.js b/ext/websocket/01_websocket.js index 680f5dab60..c6ae46920b 100644 --- a/ext/websocket/01_websocket.js +++ b/ext/websocket/01_websocket.js @@ -23,6 +23,7 @@ import { } from "ext:core/ops"; const { ArrayBufferIsView, + ArrayIsArray, ArrayPrototypeJoin, ArrayPrototypeMap, ArrayPrototypePush, @@ -64,18 +65,41 @@ import { } from "ext:deno_web/02_event.js"; import { Blob, BlobPrototype } from "ext:deno_web/09_file.js"; import { getLocationHref } from "ext:deno_web/12_location.js"; +import { + fillHeaders, + headerListFromHeaders, + headersFromHeaderList, +} from "ext:deno_fetch/20_headers.js"; -webidl.converters["sequence or DOMString"] = ( +webidl.converters["WebSocketInit"] = webidl.createDictionaryConverter( + "WebSocketInit", + [ + { + key: "headers", + converter: webidl.converters["HeadersInit"], + }, + { + key: "protocols", + converter: webidl.converters["sequence"], + }, + ], +); + +webidl.converters["WebSocketInit or sequence or DOMString"] = ( V, prefix, context, opts, ) => { - // Union for (sequence or DOMString) + // Union for (WebSocketInit or sequence or DOMString) + if (V === null || V === undefined) { + return webidl.converters["WebSocketInit"](V, prefix, context, opts); + } if (webidl.type(V) === "Object" && V !== null) { if (V[SymbolIterator] !== undefined) { return webidl.converters["sequence"](V, prefix, context, opts); } + return webidl.converters["WebSocketInit"](V, prefix, context, opts); } return webidl.converters.DOMString(V, prefix, context, opts); }; @@ -124,7 +148,7 @@ const _idleTimeoutTimeout = Symbol("[[idleTimeoutTimeout]]"); const _serverHandleIdleTimeout = Symbol("[[serverHandleIdleTimeout]]"); class WebSocket extends EventTarget { - constructor(url, protocols = []) { + constructor(url, initOrProtocols) { super(); this[webidl.brand] = webidl.brand; this[_rid] = undefined; @@ -142,11 +166,12 @@ class WebSocket extends EventTarget { const prefix = "Failed to construct 'WebSocket'"; webidl.requiredArguments(arguments.length, 1, prefix); url = webidl.converters.USVString(url, prefix, "Argument 1"); - protocols = webidl.converters["sequence or DOMString"]( - protocols, - prefix, - "Argument 2", - ); + initOrProtocols = webidl.converters + ["WebSocketInit or sequence or DOMString"]( + initOrProtocols, + prefix, + "Argument 2", + ); let wsURL; @@ -179,8 +204,20 @@ class WebSocket extends EventTarget { this[_url] = wsURL.href; this[_role] = CLIENT; - if (typeof protocols === "string") { - protocols = [protocols]; + let 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 ( @@ -224,6 +261,7 @@ class WebSocket extends EventTarget { wsURL.href, ArrayPrototypeJoin(protocols, ", "), cancelRid, + headers ? headerListFromHeaders(headers) : null, ), (create) => { this[_rid] = create.rid; diff --git a/tests/unit/websocket_test.ts b/tests/unit/websocket_test.ts index ef664038cf..e5f23072c6 100644 --- a/tests/unit/websocket_test.ts +++ b/tests/unit/websocket_test.ts @@ -1,4 +1,7 @@ // Copyright 2018-2025 the Deno authors. MIT license. + +// deno-lint-ignore-file no-console + import { assert, assertEquals, @@ -867,3 +870,68 @@ Deno.test("websocket close ongoing handshake", async () => { 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, +): ({ hostname, port }: { hostname: string; port: number }) => void { + return () => { + resolve(); + }; +} + +Deno.test("WebSocket headers", async () => { + const ac = new AbortController(); + const listeningDeferred = Promise.withResolvers(); + const doneDeferred = Promise.withResolvers(); + 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(); + 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; +}); diff --git a/tests/wpt/runner/expectation.json b/tests/wpt/runner/expectation.json index f854a9779a..2d6e4f5923 100644 --- a/tests/wpt/runner/expectation.json +++ b/tests/wpt/runner/expectation.json @@ -14554,6 +14554,7 @@ "eventhandlers.any.worker.html?wpt_flags=h2": true, "eventhandlers.any.worker.html?wss": true, "idlharness.any.html": [ + "WebSocket interface object length", "WebSocket interface: constant CONNECTING on interface object", "WebSocket interface: constant CONNECTING on interface prototype object", "WebSocket interface: constant OPEN on interface object", @@ -14572,6 +14573,7 @@ "Stringification of new CloseEvent(\"close\")" ], "idlharness.any.worker.html": [ + "WebSocket interface object length", "WebSocket interface: constant CONNECTING on interface object", "WebSocket interface: constant CONNECTING on interface prototype object", "WebSocket interface: constant OPEN on interface object",