fix(ext/websocket): cancel in-flight handshake on close() (#28598)

Fixes https://github.com/denoland/deno/issues/25126

---------

Co-authored-by: Ryan Dahl <ry@tinyclouds.org>
This commit is contained in:
Divy Srivastava 2025-03-27 20:43:32 -07:00 committed by GitHub
parent f42b39d2cf
commit 66e03a39a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 92 additions and 8 deletions

View file

@ -116,6 +116,7 @@ const _binaryType = Symbol("[[binaryType]]");
const _eventLoop = Symbol("[[eventLoop]]");
const _sendQueue = Symbol("[[sendQueue]]");
const _queueSend = Symbol("[[queueSend]]");
const _cancelHandle = Symbol("[[cancelHandle]]");
const _server = Symbol("[[server]]");
const _idleTimeoutDuration = Symbol("[[idleTimeout]]");
@ -136,6 +137,7 @@ class WebSocket extends EventTarget {
this[_idleTimeoutDuration] = 0;
this[_idleTimeoutTimeout] = undefined;
this[_sendQueue] = [];
this[_cancelHandle] = undefined;
const prefix = "Failed to construct 'WebSocket'";
webidl.requiredArguments(arguments.length, 1, prefix);
@ -177,12 +179,6 @@ class WebSocket extends EventTarget {
this[_url] = wsURL.href;
this[_role] = CLIENT;
op_ws_check_permission_and_cancel_handle(
"WebSocket.abort()",
this[_url],
false,
);
if (typeof protocols === "string") {
protocols = [protocols];
}
@ -214,11 +210,20 @@ class WebSocket extends EventTarget {
);
}
const cancelRid = op_ws_check_permission_and_cancel_handle(
"WebSocket.abort()",
this[_url],
true,
);
this[_cancelHandle] = cancelRid;
PromisePrototypeThen(
op_ws_create(
"new WebSocket()",
wsURL.href,
ArrayPrototypeJoin(protocols, ", "),
cancelRid,
),
(create) => {
this[_rid] = create.rid;
@ -256,6 +261,12 @@ class WebSocket extends EventTarget {
);
this.dispatchEvent(errorEv);
if (this[_cancelHandle]) {
core.tryClose(this[_cancelHandle]);
this[_cancelHandle] = undefined;
}
const closeEv = new CloseEvent("close");
this.dispatchEvent(closeEv);
},
@ -397,6 +408,13 @@ class WebSocket extends EventTarget {
);
}
if (this[_cancelHandle]) {
// Cancel ongoing handshake.
core.tryClose(this[_cancelHandle]);
this[_cancelHandle] = undefined;
}
if (this[_readyState] === CONNECTING) {
this[_readyState] = CLOSING;
} else if (this[_readyState] === OPEN) {

View file

@ -1,5 +1,11 @@
// Copyright 2018-2025 the Deno authors. MIT license.
import { assert, assertEquals, assertThrows, fail } from "./test_util.ts";
import {
assert,
assertEquals,
assertThrows,
delay,
fail,
} from "./test_util.ts";
const servePort = 4248;
const serveUrl = `ws://localhost:${servePort}/`;
@ -822,3 +828,42 @@ Deno.test("send to a closed socket", async () => {
};
await promise;
});
// https://github.com/denoland/deno/issues/25126
Deno.test("websocket close ongoing handshake", async () => {
// First try to close without any delay
{
const { promise, resolve } = Promise.withResolvers<void>();
let gotError1 = false;
const ws = new WebSocket("ws://localhost:4264");
ws.onopen = () => fail();
ws.onerror = (e) => {
assertEquals((e as ErrorEvent).error.code, "EINTR");
gotError1 = true;
};
ws.onclose = () => resolve();
ws.close();
await promise;
assert(gotError1);
}
await delay(50); // Wait a bit before trying again.
{
const { promise: promise2, resolve: resolve2 } = Promise.withResolvers<
void
>();
const ws2 = new WebSocket("ws://localhost:4264");
ws2.onopen = () => fail();
let gotError2 = false;
ws2.onerror = (e) => {
assertEquals((e as ErrorEvent).error.code, "EINTR");
gotError2 = true;
};
ws2.onclose = () => resolve2();
await delay(50); // wait a bit this time before calling close
ws2.close();
await promise2;
assert(gotError2);
}
});

View file

@ -323,7 +323,7 @@ async fn get_tcp_listener_stream(
futures::stream::select_all(listeners)
}
pub const TEST_SERVERS_COUNT: usize = 33;
pub const TEST_SERVERS_COUNT: usize = 34;
#[derive(Default)]
struct HttpServerCount {

View file

@ -84,6 +84,7 @@ const WS_PORT: u16 = 4242;
const WSS_PORT: u16 = 4243;
const WSS2_PORT: u16 = 4249;
const WS_CLOSE_PORT: u16 = 4244;
const WS_HANG_PORT: u16 = 4264;
const WS_PING_PORT: u16 = 4245;
const H2_GRPC_PORT: u16 = 4246;
const H2S_GRPC_PORT: u16 = 4247;
@ -120,6 +121,7 @@ pub async fn run_all_servers() {
let ws_ping_server_fut = ws::run_ws_ping_server(WS_PING_PORT);
let wss_server_fut = ws::run_wss_server(WSS_PORT);
let ws_close_server_fut = ws::run_ws_close_server(WS_CLOSE_PORT);
let ws_hang_server_fut = ws::run_ws_hang_handshake(WS_HANG_PORT);
let wss2_server_fut = ws::run_wss2_server(WSS2_PORT);
let tls_server_fut = run_tls_server(TLS_PORT);
@ -162,6 +164,7 @@ pub async fn run_all_servers() {
tls_server_fut.boxed_local(),
tls_client_auth_server_fut.boxed_local(),
ws_close_server_fut.boxed_local(),
ws_hang_server_fut.boxed_local(),
another_redirect_server_fut.boxed_local(),
auth_redirect_server_fut.boxed_local(),
basic_auth_redirect_server_fut.boxed_local(),

View file

@ -62,6 +62,24 @@ pub async fn run_ws_close_server(port: u16) {
}
}
pub async fn run_ws_hang_handshake(port: u16) {
let mut tcp = get_tcp_listener_stream("ws (hang handshake)", port).await;
while let Some(Ok(mut stream)) = tcp.next().await {
loop {
let mut buf = [0; 1024];
let n = stream.read(&mut buf).await;
if n.is_err() {
break;
}
if n.unwrap() == 0 {
break;
}
}
}
}
pub async fn run_wss2_server(port: u16) {
let mut tls = get_tls_listener_stream(
"wss2 (tls)",