From 5de96549fd6315a1d8f77df7352189bfb4a66bf0 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Tue, 27 Jan 2026 07:09:29 +0000 Subject: [PATCH] fix(websocket): accept rejectUnauthorized at top level of options For Node.js ws library compatibility, accept rejectUnauthorized both at the top level and nested inside the tls object. The tls object takes precedence if both are specified. Fixes #22870 Co-Authored-By: Claude Opus 4.5 --- src/bun.js/bindings/webcore/JSWebSocket.cpp | 10 ++ test/regression/issue/22870.test.ts | 163 ++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 test/regression/issue/22870.test.ts diff --git a/src/bun.js/bindings/webcore/JSWebSocket.cpp b/src/bun.js/bindings/webcore/JSWebSocket.cpp index b26cc913d3..e45d46a027 100644 --- a/src/bun.js/bindings/webcore/JSWebSocket.cpp +++ b/src/bun.js/bindings/webcore/JSWebSocket.cpp @@ -267,6 +267,16 @@ static inline JSC::EncodedJSValue constructJSWebSocket3(JSGlobalObject* lexicalG RETURN_IF_EXCEPTION(throwScope, {}); } + // Check for rejectUnauthorized at top level for Node.js ws library compatibility + // Only check if it wasn't already set from the tls object + if (rejectUnauthorized == -1) { + auto rejectUnauthorizedValue = Bun::getOwnPropertyIfExists(globalObject, options, PropertyName(Identifier::fromString(vm, "rejectUnauthorized"_s))); + RETURN_IF_EXCEPTION(throwScope, {}); + if (rejectUnauthorizedValue && !rejectUnauthorizedValue.isUndefinedOrNull() && rejectUnauthorizedValue.isBoolean()) { + rejectUnauthorized = rejectUnauthorizedValue.asBoolean() ? 1 : 0; + } + } + // Parse proxy option - can be string or { url, headers } auto proxyValue = Bun::getOwnPropertyIfExists(globalObject, options, PropertyName(Identifier::fromString(vm, "proxy"_s))); RETURN_IF_EXCEPTION(throwScope, {}); diff --git a/test/regression/issue/22870.test.ts b/test/regression/issue/22870.test.ts new file mode 100644 index 0000000000..2a0bfbe9ea --- /dev/null +++ b/test/regression/issue/22870.test.ts @@ -0,0 +1,163 @@ +import { describe, expect, it } from "bun:test"; +import { tls } from "harness"; + +// Test for https://github.com/oven-sh/bun/issues/22870 +// rejectUnauthorized should work at the top level of WebSocket options +// for compatibility with Node.js ws library +describe("WebSocket rejectUnauthorized option", () => { + it("should accept rejectUnauthorized at top level", async () => { + // Create a server with self-signed certificate + using server = Bun.serve({ + port: 0, + tls, + fetch(req, server) { + if (server.upgrade(req)) { + return; + } + return new Response("Upgrade failed", { status: 500 }); + }, + websocket: { + open(ws) { + ws.send("hello"); + }, + message(ws, message) { + ws.send(message); + }, + }, + }); + + // This should work with rejectUnauthorized at top level + const ws = new WebSocket(server.url.href.replace("https:", "wss:"), { + rejectUnauthorized: false, + }); + + await new Promise((resolve, reject) => { + ws.onopen = () => resolve(); + ws.onerror = e => reject(new Error(`WebSocket error: ${e}`)); + }); + + const closed = new Promise(resolve => { + ws.onclose = resolve; + }); + ws.close(); + await closed; + }); + + it("should still accept rejectUnauthorized nested in tls object", async () => { + // Create a server with self-signed certificate + using server = Bun.serve({ + port: 0, + tls, + fetch(req, server) { + if (server.upgrade(req)) { + return; + } + return new Response("Upgrade failed", { status: 500 }); + }, + websocket: { + open(ws) { + ws.send("hello"); + }, + message(ws, message) { + ws.send(message); + }, + }, + }); + + // The nested tls object should still work + const ws = new WebSocket(server.url.href.replace("https:", "wss:"), { + tls: { rejectUnauthorized: false }, + }); + + await new Promise((resolve, reject) => { + ws.onopen = () => resolve(); + ws.onerror = e => reject(new Error(`WebSocket error: ${e}`)); + }); + + const closed = new Promise(resolve => { + ws.onclose = resolve; + }); + ws.close(); + await closed; + }); + + it("should prefer tls.rejectUnauthorized over top-level rejectUnauthorized", async () => { + // Create a server with self-signed certificate + using server = Bun.serve({ + port: 0, + tls, + fetch(req, server) { + if (server.upgrade(req)) { + return; + } + return new Response("Upgrade failed", { status: 500 }); + }, + websocket: { + open(ws) { + ws.send("hello"); + }, + message(ws, message) { + ws.send(message); + }, + }, + }); + + // When both are specified, tls.rejectUnauthorized should take precedence + // Here tls.rejectUnauthorized: false should allow connection even though top-level says true + const ws = new WebSocket(server.url.href.replace("https:", "wss:"), { + rejectUnauthorized: true, + tls: { rejectUnauthorized: false }, + }); + + await new Promise((resolve, reject) => { + ws.onopen = () => resolve(); + ws.onerror = e => reject(new Error(`WebSocket error: ${e}`)); + }); + + const closed = new Promise(resolve => { + ws.onclose = resolve; + }); + ws.close(); + await closed; + }); + + it("should fail with rejectUnauthorized: true against self-signed cert", async () => { + // Create a server with self-signed certificate + using server = Bun.serve({ + port: 0, + tls, + fetch(req, server) { + if (server.upgrade(req)) { + return; + } + return new Response("Upgrade failed", { status: 500 }); + }, + websocket: { + open(ws) { + ws.send("hello"); + }, + message(ws, message) { + ws.send(message); + }, + }, + }); + + // With rejectUnauthorized: true (or default), self-signed cert should be rejected + const ws = new WebSocket(server.url.href.replace("https:", "wss:"), { + rejectUnauthorized: true, + }); + + const errored = await new Promise(resolve => { + ws.onopen = () => { + ws.close(); + resolve(false); + }; + ws.onerror = () => { + ws.close(); + resolve(true); + }; + }); + + expect(errored).toBe(true); + }); +});