mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 02:48:50 +00:00
## Summary This PR fixes WebSocket to correctly emit an `error` event before the `close` event when the handshake fails (e.g., 302 redirects, non-101 status codes, missing headers). Fixes #14338 ## Problem Previously, when a WebSocket connection failed during handshake (like receiving a 302 redirect or connecting to a non-WebSocket server), Bun would only emit a `close` event. This behavior differed from the WHATWG WebSocket specification and other runtimes (browsers, Node.js with `ws`, Deno) which emit both `error` and `close` events. ## Solution Modified `WebSocket::didFailWithErrorCode()` in `WebSocket.cpp` to pass `isConnectionError = true` for all handshake failure error codes, ensuring an error event is dispatched before the close event when the connection is in the CONNECTING state. ## Changes - Updated error handling in `src/bun.js/bindings/webcore/WebSocket.cpp` to emit error events for handshake failures - Added comprehensive test coverage in `test/regression/issue/14338.test.ts` ## Test Coverage The test file includes: 1. **Negative test**: 302 redirect response - verifies error event is emitted 2. **Negative test**: Non-WebSocket HTTP server - verifies error event is emitted 3. **Positive test**: Successful WebSocket connection - verifies NO error event is emitted All tests pass with the fix applied. 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude Bot <claude-bot@bun.sh> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Jarred Sumner <jarred@jarredsumner.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
151 lines
3.9 KiB
TypeScript
151 lines
3.9 KiB
TypeScript
import { expect, test } from "bun:test";
|
|
|
|
test("WebSocket should emit error event before close event on handshake failure (issue #14338)", async () => {
|
|
const { promise: errorPromise, resolve: resolveError } = Promise.withResolvers<Event>();
|
|
const { promise: closePromise, resolve: resolveClose } = Promise.withResolvers<CloseEvent>();
|
|
const events: string[] = [];
|
|
|
|
// Create a server that returns a 302 redirect response instead of a WebSocket upgrade
|
|
await using server = Bun.serve({
|
|
port: 0,
|
|
fetch(req) {
|
|
// Return a 302 redirect response to simulate handshake failure
|
|
return new Response(null, {
|
|
status: 302,
|
|
headers: {
|
|
Location: "http://example.com",
|
|
},
|
|
});
|
|
},
|
|
});
|
|
|
|
const ws = new WebSocket(`ws://localhost:${server.port}`);
|
|
|
|
ws.addEventListener("error", event => {
|
|
events.push("error");
|
|
resolveError(event);
|
|
});
|
|
|
|
ws.addEventListener("close", event => {
|
|
events.push("close");
|
|
resolveClose(event);
|
|
});
|
|
|
|
ws.addEventListener("open", () => {
|
|
events.push("open");
|
|
});
|
|
|
|
// Wait for close event (which should always fire)
|
|
await closePromise;
|
|
|
|
// After the fix, both error and close events should be emitted
|
|
// The error event should come before the close event
|
|
expect(events).toEqual(["error", "close"]);
|
|
});
|
|
|
|
test("WebSocket successful connection should NOT emit error event", async () => {
|
|
const { promise: openPromise, resolve: resolveOpen } = Promise.withResolvers<Event>();
|
|
const { promise: messagePromise, resolve: resolveMessage } = Promise.withResolvers<MessageEvent>();
|
|
const { promise: closePromise, resolve: resolveClose } = Promise.withResolvers<CloseEvent>();
|
|
const events: string[] = [];
|
|
|
|
// Create a proper WebSocket server
|
|
await using server = Bun.serve({
|
|
port: 0,
|
|
websocket: {
|
|
message(ws, message) {
|
|
ws.send(message);
|
|
},
|
|
},
|
|
fetch(req, server) {
|
|
if (server.upgrade(req)) {
|
|
return;
|
|
}
|
|
return new Response("Not found", { status: 404 });
|
|
},
|
|
});
|
|
|
|
const ws = new WebSocket(`ws://localhost:${server.port}`);
|
|
|
|
ws.addEventListener("error", event => {
|
|
events.push("error");
|
|
});
|
|
|
|
ws.addEventListener("open", event => {
|
|
events.push("open");
|
|
resolveOpen(event);
|
|
});
|
|
|
|
ws.addEventListener("message", event => {
|
|
events.push("message");
|
|
resolveMessage(event);
|
|
});
|
|
|
|
ws.addEventListener("close", event => {
|
|
events.push("close");
|
|
resolveClose(event);
|
|
});
|
|
|
|
// Wait for connection to open
|
|
await openPromise;
|
|
|
|
// Send a test message
|
|
ws.send("test");
|
|
|
|
// Wait for echo
|
|
const msg = await messagePromise;
|
|
expect(msg.data).toBe("test");
|
|
|
|
// Close the connection normally
|
|
ws.close();
|
|
|
|
// Wait for close event
|
|
await closePromise;
|
|
|
|
// Should have open, message, and close events, but NO error event
|
|
expect(events).toContain("open");
|
|
expect(events).toContain("message");
|
|
expect(events).toContain("close");
|
|
expect(events).not.toContain("error");
|
|
});
|
|
|
|
test("WebSocket should emit error and close events on connection to non-WebSocket server", async () => {
|
|
const { promise: closePromise, resolve: resolveClose } = Promise.withResolvers<CloseEvent>();
|
|
const events: string[] = [];
|
|
|
|
// Create a regular HTTP server (not WebSocket)
|
|
await using server = Bun.serve({
|
|
port: 0,
|
|
fetch(req) {
|
|
// Return a normal HTTP response
|
|
return new Response("Not a WebSocket server", {
|
|
status: 200,
|
|
headers: {
|
|
"Content-Type": "text/plain",
|
|
},
|
|
});
|
|
},
|
|
});
|
|
|
|
const ws = new WebSocket(`ws://localhost:${server.port}`);
|
|
|
|
ws.addEventListener("error", event => {
|
|
events.push("error");
|
|
});
|
|
|
|
ws.addEventListener("close", event => {
|
|
events.push("close");
|
|
resolveClose(event);
|
|
});
|
|
|
|
ws.addEventListener("open", () => {
|
|
events.push("open");
|
|
});
|
|
|
|
// Wait for close event
|
|
await closePromise;
|
|
|
|
// After the fix, both error and close events should be emitted
|
|
expect(events).toEqual(["error", "close"]);
|
|
});
|