diff --git a/cli/tsc/dts/lib.deno.unstable.d.ts b/cli/tsc/dts/lib.deno.unstable.d.ts index 21d006fad3..056d8e6099 100644 --- a/cli/tsc/dts/lib.deno.unstable.d.ts +++ b/cli/tsc/dts/lib.deno.unstable.d.ts @@ -2227,6 +2227,26 @@ declare var WebSocketStream: { new (url: string, options?: WebSocketStreamOptions): WebSocketStream; }; +/** **UNSTABLE**: New API, yet to be vetted. + * + * @tags allow-net + * @category Web Sockets + */ +declare interface WebSocketError extends DOMException { + readonly closeCode: number; + readonly reason: string; +} + +/** **UNSTABLE**: New API, yet to be vetted. + * + * @tags allow-net + * @category Web Sockets + */ +declare var WebSocketError: { + readonly prototype: WebSocketError; + new (message?: string, init?: WebSocketCloseInfo): WebSocketError; +}; + // Adapted from `tc39/proposal-temporal`: https://github.com/tc39/proposal-temporal/blob/main/polyfill/index.d.ts /** diff --git a/ext/websocket/02_websocketstream.js b/ext/websocket/02_websocketstream.js index 4eafac88b6..01aef45d32 100644 --- a/ext/websocket/02_websocketstream.js +++ b/ext/websocket/02_websocketstream.js @@ -18,7 +18,6 @@ const { ArrayPrototypeJoin, ArrayPrototypeMap, DateNow, - Error, ObjectPrototypeIsPrototypeOf, PromisePrototypeCatch, PromisePrototypeThen, @@ -68,8 +67,12 @@ webidl.converters.WebSocketCloseInfo = webidl.createDictionaryConverter( "WebSocketCloseInfo", [ { - key: "code", - converter: webidl.converters["unsigned short"], + key: "closeCode", + converter: (V, prefix, context, opts) => + webidl.converters["unsigned short"](V, prefix, context, { + ...opts, + enforceRange: true, + }), }, { key: "reason", @@ -189,20 +192,14 @@ class WebSocketStream { } })(), () => { - const err = new DOMException( - "Closed while connecting", - "NetworkError", - ); + const err = new WebSocketError("Closed while connecting"); this[_opened].reject(err); this[_closed].reject(err); }, ); }, () => { - const err = new DOMException( - "Closed while connecting", - "NetworkError", - ); + const err = new WebSocketError("Closed while connecting"); this[_opened].reject(err); this[_closed].reject(err); }, @@ -225,17 +222,26 @@ class WebSocketStream { ); } }, - close: async (reason) => { - try { - this.close(reason?.code !== undefined ? reason : {}); - } catch (_) { - this.close(); - } + close: async () => { + this.close(); await this.closed; }, abort: async (reason) => { + let closeCode = null; + let reasonString = ""; + + if ( + ObjectPrototypeIsPrototypeOf(WebSocketErrorPrototype, reason) + ) { + closeCode = reason.closeCode; + reasonString = reason.reason; + } + try { - this.close(reason?.code !== undefined ? reason : {}); + this.close({ + closeCode, + reason: reasonString, + }); } catch (_) { this.close(); } @@ -261,7 +267,7 @@ class WebSocketStream { } case 3: { /* error */ - const err = new Error(op_ws_get_error(this[_rid])); + const err = new WebSocketError(op_ws_get_error(this[_rid])); this[_closed].reject(err); controller.error(err); core.tryClose(this[_rid]); @@ -269,7 +275,7 @@ class WebSocketStream { } case 1005: { /* closed */ - this[_closed].resolve({ code: 1005, reason: "" }); + this[_closed].resolve({ closeCode: 1005, reason: "" }); core.tryClose(this[_rid]); break; } @@ -277,7 +283,7 @@ class WebSocketStream { /* close */ const reason = op_ws_get_error(this[_rid]); this[_closed].resolve({ - code: kind, + closeCode: kind, reason, }); core.tryClose(this[_rid]); @@ -297,7 +303,7 @@ class WebSocketStream { } const error = op_ws_get_error(this[_rid]); - this[_closed].reject(new Error(error)); + this[_closed].reject(new WebSocketError(error)); core.tryClose(this[_rid]); } }; @@ -327,8 +333,21 @@ class WebSocketStream { }, pull, cancel: async (reason) => { + let closeCode = null; + let reasonString = ""; + + if ( + ObjectPrototypeIsPrototypeOf(WebSocketErrorPrototype, reason) + ) { + closeCode = reason.closeCode; + reasonString = reason.reason; + } + try { - this.close(reason?.code !== undefined ? reason : {}); + this.close({ + closeCode, + reason: reasonString, + }); } catch (_) { this.close(); } @@ -350,6 +369,7 @@ class WebSocketStream { err = options.signal.reason; } else { core.tryClose(cancelRid); + err = new WebSocketError(err.message); } this[_opened].reject(err); this[_closed].reject(err); @@ -380,38 +400,17 @@ class WebSocketStream { "Argument 1", ); - if ( - closeInfo.code && - !(closeInfo.code === 1000 || - (3000 <= closeInfo.code && closeInfo.code < 5000)) - ) { - throw new DOMException( - "The close code must be either 1000 or in the range of 3000 to 4999.", - "InvalidAccessError", - ); - } + validateCloseCodeAndReason(closeInfo); - const encoder = new TextEncoder(); - if ( - closeInfo.reason && - TypedArrayPrototypeGetByteLength(encoder.encode(closeInfo.reason)) > 123 - ) { - throw new DOMException( - "The close reason may not be longer than 123 bytes.", - "SyntaxError", - ); - } - - let code = closeInfo.code; - if (closeInfo.reason && code === undefined) { - code = 1000; + if (closeInfo.reason && closeInfo.closeCode === null) { + closeInfo.closeCode = 1000; } if (this[_opened].state === "pending") { this[_earlyClose] = true; } else if (this[_closed].state === "pending") { PromisePrototypeThen( - op_ws_close(this[_rid], code, closeInfo.reason), + op_ws_close(this[_rid], closeInfo.closeCode, closeInfo.reason), () => { setTimeout(() => { this[_closeSent].resolve(DateNow()); @@ -419,6 +418,7 @@ class WebSocketStream { }, (err) => { this[_rid] && core.tryClose(this[_rid]); + err = new WebSocketError(err.message); this[_closed].reject(err); }, ); @@ -440,7 +440,87 @@ class WebSocketStream { ); } } - const WebSocketStreamPrototype = WebSocketStream.prototype; -export { WebSocketStream }; +function validateCloseCodeAndReason(closeInfo) { + if (!closeInfo.closeCode) { + closeInfo.closeCode = null; + } + + if ( + closeInfo.closeCode && + !(closeInfo.closeCode === 1000 || + (3000 <= closeInfo.closeCode && closeInfo.closeCode < 5000)) + ) { + throw new DOMException( + "The close code must be either 1000 or in the range of 3000 to 4999.", + "InvalidAccessError", + ); + } + + const encoder = new TextEncoder(); + if ( + closeInfo.reason && + TypedArrayPrototypeGetByteLength(encoder.encode(closeInfo.reason)) > 123 + ) { + throw new DOMException( + "The close reason may not be longer than 123 bytes.", + "SyntaxError", + ); + } +} + +class WebSocketError extends DOMException { + #closeCode; + #reason; + + constructor(message = "", init = {}) { + super(message, "WebSocketError"); + this[webidl.brand] = webidl.brand; + + init = webidl.converters["WebSocketCloseInfo"]( + init, + "Failed to construct 'WebSocketError'", + "Argument 2", + ); + + validateCloseCodeAndReason(init); + + if (init.reason && init.closeCode === null) { + init.closeCode = 1000; + } + + this.#closeCode = init.closeCode; + this.#reason = init.reason; + } + + get closeCode() { + webidl.assertBranded(this, WebSocketErrorPrototype); + return this.#closeCode; + } + + get reason() { + webidl.assertBranded(this, WebSocketErrorPrototype); + return this.#reason; + } + + [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) { + return inspect( + createFilteredInspectProxy({ + object: this, + evaluate: ObjectPrototypeIsPrototypeOf(WebSocketErrorPrototype, this), + keys: [ + "message", + "name", + "closeCode", + "reason", + ], + }), + inspectOptions, + ); + } +} +webidl.configureInterface(WebSocketError); +const WebSocketErrorPrototype = WebSocketError.prototype; + +export { WebSocketError, WebSocketStream }; diff --git a/runtime/js/98_global_scope_shared.js b/runtime/js/98_global_scope_shared.js index aba0f3710f..e7504143e2 100644 --- a/runtime/js/98_global_scope_shared.js +++ b/runtime/js/98_global_scope_shared.js @@ -151,6 +151,7 @@ unstableForWindowOrWorkerGlobalScope[unstableIds.broadcastChannel] = { }; unstableForWindowOrWorkerGlobalScope[unstableIds.net] = { WebSocketStream: core.propNonEnumerable(webSocketStream.WebSocketStream), + WebSocketError: core.propNonEnumerable(webSocketStream.WebSocketError), }; // deno-fmt-ignore unstableForWindowOrWorkerGlobalScope[unstableIds.webgpu] = { diff --git a/tests/integration/lsp_tests.rs b/tests/integration/lsp_tests.rs index 5ec3117e65..6821eb8da5 100644 --- a/tests/integration/lsp_tests.rs +++ b/tests/integration/lsp_tests.rs @@ -5144,7 +5144,7 @@ fn lsp_jsr_auto_import_completion() { json!({ "triggerKind": 1 }), ); assert!(!list.is_incomplete); - assert_eq!(list.items.len(), 262); + assert_eq!(list.items.len(), 263); let item = list.items.iter().find(|i| i.label == "add").unwrap(); assert_eq!(&item.label, "add"); assert_eq!( @@ -5224,7 +5224,7 @@ fn lsp_jsr_auto_import_completion_import_map() { json!({ "triggerKind": 1 }), ); assert!(!list.is_incomplete); - assert_eq!(list.items.len(), 262); + assert_eq!(list.items.len(), 263); let item = list.items.iter().find(|i| i.label == "add").unwrap(); assert_eq!(&item.label, "add"); assert_eq!(json!(&item.label_details), json!({ "description": "add" })); diff --git a/tests/wpt/runner/expectation.json b/tests/wpt/runner/expectation.json index 7adeabdc0f..ec917c497c 100644 --- a/tests/wpt/runner/expectation.json +++ b/tests/wpt/runner/expectation.json @@ -2164,7 +2164,26 @@ "importVectorKeys step: EdDSA Ed448 verification failure due to shortened signature", "importVectorKeys step: EdDSA Ed448 verification failure due to altered data", "importVectorKeys step: EdDSA Ed448 signing with wrong algorithm name", - "importVectorKeys step: EdDSA Ed448 verifying with wrong algorithm name" + "importVectorKeys step: EdDSA Ed448 verifying with wrong algorithm name", + "EdDSA Ed448 verification", + "EdDSA Ed448 verification with altered signature after call", + "EdDSA Ed448 with altered data after call", + "EdDSA Ed448 using privateKey to verify", + "EdDSA Ed448 using publicKey to sign", + "EdDSA Ed448 no verify usage", + "EdDSA Ed448 round trip", + "EdDSA Ed448 signing with wrong algorithm name", + "EdDSA Ed448 verifying with wrong algorithm name", + "EdDSA Ed448 verification failure due to altered signature", + "EdDSA Ed448 verification failure due to shortened signature", + "EdDSA Ed448 verification failure due to altered data", + "Ed25519 Verification checks with small-order key of order - Test 0", + "Ed25519 Verification checks with small-order key of order - Test 1", + "Ed25519 Verification checks with small-order key of order - Test 2", + "Ed25519 Verification checks with small-order key of order - Test 3", + "Ed25519 Verification checks with small-order key of order - Test 11", + "Ed25519 Verification checks with small-order key of order - Test 12", + "Ed25519 Verification checks with small-order key of order - Test 13" ], "eddsa.https.any.worker.html": [ "Sign and verify using generated Ed448 keys.", @@ -2179,7 +2198,26 @@ "importVectorKeys step: EdDSA Ed448 verification failure due to shortened signature", "importVectorKeys step: EdDSA Ed448 verification failure due to altered data", "importVectorKeys step: EdDSA Ed448 signing with wrong algorithm name", - "importVectorKeys step: EdDSA Ed448 verifying with wrong algorithm name" + "importVectorKeys step: EdDSA Ed448 verifying with wrong algorithm name", + "EdDSA Ed448 verification", + "EdDSA Ed448 verification with altered signature after call", + "EdDSA Ed448 with altered data after call", + "EdDSA Ed448 using privateKey to verify", + "EdDSA Ed448 using publicKey to sign", + "EdDSA Ed448 no verify usage", + "EdDSA Ed448 round trip", + "EdDSA Ed448 signing with wrong algorithm name", + "EdDSA Ed448 verifying with wrong algorithm name", + "EdDSA Ed448 verification failure due to altered signature", + "EdDSA Ed448 verification failure due to shortened signature", + "EdDSA Ed448 verification failure due to altered data", + "Ed25519 Verification checks with small-order key of order - Test 0", + "Ed25519 Verification checks with small-order key of order - Test 1", + "Ed25519 Verification checks with small-order key of order - Test 2", + "Ed25519 Verification checks with small-order key of order - Test 3", + "Ed25519 Verification checks with small-order key of order - Test 11", + "Ed25519 Verification checks with small-order key of order - Test 12", + "Ed25519 Verification checks with small-order key of order - Test 13" ] }, "algorithm-discards-context.https.window.html": false @@ -6903,10 +6941,7 @@ }, "the-iframe-element": { "cross-origin-to-whom-part-2.window.html": false, - "cross-origin-to-whom.window.html": false, - "sandbox-top-navigation-child.tentative.sub.window.html": false, - "sandbox-top-navigation-escalate-privileges.tentative.sub.window.html": false, - "sandbox-top-navigation-grandchild.tentative.sub.window.html": false + "cross-origin-to-whom.window.html": false }, "the-img-element": { "historical-progress-event.window.html": false @@ -8162,14 +8197,21 @@ "backpressure-send.any.html?wss": true, "backpressure-send.any.worker.html?wpt_flags=h2": false, "backpressure-send.any.worker.html?wss": true, - "close.any.html?wpt_flags=h2": true, - "close.any.html?wss": true, - "close.any.worker.html?wpt_flags=h2": true, - "close.any.worker.html?wss": true, + "close.any.html?wpt_flags=h2": false, + "close.any.html?wss": [ + "incomplete closing handshake should be considered unclean close" + ], + "close.any.worker.html?wpt_flags=h2": false, + "close.any.worker.html?wss": [ + "incomplete closing handshake should be considered unclean close" + ], "constructor.any.html?wpt_flags=h2": true, "constructor.any.html?wss": true, "constructor.any.worker.html?wpt_flags=h2": true, - "constructor.any.worker.html?wss": true + "constructor.any.worker.html?wss": true, + "close.any.html?default": [ + "incomplete closing handshake should be considered unclean close" + ] } } }, diff --git a/tests/wpt/suite b/tests/wpt/suite index fb81ba9b33..acabb88c58 160000 --- a/tests/wpt/suite +++ b/tests/wpt/suite @@ -1 +1 @@ -Subproject commit fb81ba9b33964b3177b4f6c1715d5f46c325b443 +Subproject commit acabb88c580459224955c8c0fa0bddd6fd701f30