Files
bun.sh/test/regression/issue/14338.test.ts
robobun 2e8e7a000c Fix WebSocket to emit error event before close on handshake failure (#22325)
## 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>
2025-09-02 03:26:51 -07:00

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"]);
});