From 6f1d8294444b75d1aa59e643ab8bf67e4ad3a297 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Thu, 16 Apr 2026 13:56:24 -0700 Subject: [PATCH 1/2] Make WebSocket.close() idempotent per WHATWG spec Previously, calling close() on a WebSocket that was already CLOSING or CLOSED would throw 'Close has already been called.' This violates the WHATWG WebSocket specification, which requires close() to be a no-op in those states. The bug can surface as an uncaught async exception when a delegate close/error callback fires after user code has already called close(), or when user code calls close() twice in rapid succession (for example, from both a catch block and an onmessage handler). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Polyfills/WebSocket/Source/WebSocket.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Polyfills/WebSocket/Source/WebSocket.cpp b/Polyfills/WebSocket/Source/WebSocket.cpp index bcedb95d..7202507a 100644 --- a/Polyfills/WebSocket/Source/WebSocket.cpp +++ b/Polyfills/WebSocket/Source/WebSocket.cpp @@ -51,11 +51,13 @@ namespace Babylon::Polyfills::Internal m_cancellationSource->cancel(); } - void WebSocket::Close(const Napi::CallbackInfo& info) + void WebSocket::Close(const Napi::CallbackInfo&) { + // Per the WHATWG WebSocket spec, calling close() on a socket that is + // already CLOSING or CLOSED is a no-op. if (m_readyState == ReadyState::Closed || m_readyState == ReadyState::Closing) { - throw Napi::Error::New(info.Env(), "Close has already been called."); + return; } m_readyState = ReadyState::Closing; m_webSocket.Close(); From a1cedb797e24bd141bcc3a61106065f1b8cafc31 Mon Sep 17 00:00:00 2001 From: Gary Hsu Date: Thu, 16 Apr 2026 13:59:36 -0700 Subject: [PATCH 2/2] Make WebSocket.send() spec-compliant when CLOSING or CLOSED Per the WHATWG WebSocket spec, send() throws InvalidStateError only when readyState is CONNECTING. When CLOSING or CLOSED, the call silently discards the data instead of throwing. The previous behavior of throwing on any non-OPEN state was too strict and could surface as an uncaught exception when a send raced with a close (e.g. a scheduled send issued between close() being called and the onclose callback firing). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- Polyfills/WebSocket/Source/WebSocket.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Polyfills/WebSocket/Source/WebSocket.cpp b/Polyfills/WebSocket/Source/WebSocket.cpp index 7202507a..0f6f7897 100644 --- a/Polyfills/WebSocket/Source/WebSocket.cpp +++ b/Polyfills/WebSocket/Source/WebSocket.cpp @@ -65,9 +65,17 @@ namespace Babylon::Polyfills::Internal void WebSocket::Send(const Napi::CallbackInfo& info) { + // Per the WHATWG WebSocket spec, send() throws InvalidStateError only + // when readyState is CONNECTING. When CLOSING or CLOSED, the data is + // silently discarded (the spec still bumps bufferedAmount, which this + // polyfill does not track). + if (m_readyState == ReadyState::Connecting) + { + throw Napi::Error::New(info.Env(), "WebSocket is still in CONNECTING state."); + } if (m_readyState != ReadyState::Open) { - throw Napi::Error::New(info.Env(), "Websocket readyState is not open."); + return; } std::string message = info[0].As(); m_webSocket.Send(message);