Files
bun.sh/test/regression/issue/3613.test.ts
robobun 12243b9715 fix(ws): pass selected protocol from handleProtocols to upgrade response (#26118)
## Summary
- Fixes the `handleProtocols` option not setting the selected protocol
in WebSocket upgrade responses
- Removes duplicate protocol header values in responses

## Test plan
- Added regression tests in `test/regression/issue/3613.test.ts`
- Verified using fetch to check actual response headers contain the
correct protocol

Fixes #3613

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude Opus 4.5 <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>
2026-01-15 18:22:01 -08:00

181 lines
4.9 KiB
TypeScript

import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
// https://github.com/oven-sh/bun/issues/3613
// WebSocketServer handleProtocols option should set the selected protocol in the upgrade response
test("ws WebSocketServer handleProtocols sets selected protocol", async () => {
using dir = tempDir("ws-handle-protocols", {
"server.js": `
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({
port: 0,
handleProtocols: (protocols, request) => {
return 'selected-protocol';
}
});
wss.on('listening', async () => {
const port = wss.address().port;
console.log('PORT:' + port);
// Test using fetch to verify the actual response headers
try {
const res = await fetch('http://127.0.0.1:' + port, {
headers: {
"Upgrade": "websocket",
"Connection": "Upgrade",
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
"Sec-WebSocket-Version": "13",
"Sec-WebSocket-Protocol": "custom-protocol, selected-protocol"
}
});
console.log("STATUS:" + res.status);
console.log("PROTOCOL:" + res.headers.get("sec-websocket-protocol"));
} catch (e) {
console.log("ERROR:" + e.message);
}
wss.close();
process.exit(0);
});
wss.on('connection', (ws) => {
console.log('SERVER_WS_PROTOCOL:' + ws.protocol);
});
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "server.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// The server should respond with the protocol selected by handleProtocols
expect(stdout).toContain("STATUS:101");
expect(stdout).toContain("PROTOCOL:selected-protocol");
expect(stdout).toContain("SERVER_WS_PROTOCOL:selected-protocol");
expect(exitCode).toBe(0);
});
test("ws WebSocketServer handleProtocols with no protocol", async () => {
using dir = tempDir("ws-handle-protocols-empty", {
"server.js": `
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({
port: 0,
handleProtocols: (protocols, request) => {
// Return empty string - should not set a protocol header
return '';
}
});
wss.on('listening', async () => {
const port = wss.address().port;
console.log('PORT:' + port);
try {
const res = await fetch('http://127.0.0.1:' + port, {
headers: {
"Upgrade": "websocket",
"Connection": "Upgrade",
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
"Sec-WebSocket-Version": "13",
"Sec-WebSocket-Protocol": "custom-protocol"
}
});
console.log("STATUS:" + res.status);
// When handleProtocols returns empty, Bun falls back to client's first protocol
console.log("PROTOCOL:" + res.headers.get("sec-websocket-protocol"));
} catch (e) {
console.log("ERROR:" + e.message);
}
wss.close();
process.exit(0);
});
wss.on('connection', (ws) => {
console.log('SERVER_WS_PROTOCOL:' + JSON.stringify(ws.protocol));
});
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "server.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// The server should respond with 101 status
expect(stdout).toContain("STATUS:101");
expect(exitCode).toBe(0);
});
test("ws WebSocketServer without handleProtocols uses first client protocol", async () => {
using dir = tempDir("ws-no-handle-protocols", {
"server.js": `
import { WebSocketServer } from 'ws';
const wss = new WebSocketServer({
port: 0,
// No handleProtocols - should default to first client protocol
});
wss.on('listening', async () => {
const port = wss.address().port;
console.log('PORT:' + port);
try {
const res = await fetch('http://127.0.0.1:' + port, {
headers: {
"Upgrade": "websocket",
"Connection": "Upgrade",
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
"Sec-WebSocket-Version": "13",
"Sec-WebSocket-Protocol": "first-protocol, second-protocol"
}
});
console.log("STATUS:" + res.status);
console.log("PROTOCOL:" + res.headers.get("sec-websocket-protocol"));
} catch (e) {
console.log("ERROR:" + e.message);
}
wss.close();
process.exit(0);
});
wss.on('connection', (ws) => {
console.log('SERVER_WS_PROTOCOL:' + ws.protocol);
});
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "server.js"],
cwd: String(dir),
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// Without handleProtocols, should default to first client protocol
expect(stdout).toContain("STATUS:101");
expect(stdout).toContain("PROTOCOL:first-protocol");
expect(stdout).toContain("SERVER_WS_PROTOCOL:first-protocol");
expect(exitCode).toBe(0);
});