mirror of
https://github.com/oven-sh/bun
synced 2026-02-17 06:12:08 +00:00
Compare commits
2 Commits
claude/add
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1eb4070bb | ||
|
|
3b8eb7426f |
@@ -299,10 +299,12 @@ pub const Response = struct {
|
||||
);
|
||||
|
||||
return switch (rc) {
|
||||
-1 => if (comptime Environment.allow_assert) brk: {
|
||||
Output.debug("Malformed HTTP response:\n{s}", .{buf});
|
||||
-1 => brk: {
|
||||
if (comptime Environment.allow_assert) {
|
||||
Output.debug("Malformed HTTP response:\n{s}", .{buf});
|
||||
}
|
||||
break :brk error.Malformed_HTTP_Response;
|
||||
} else error.Malformed_HTTP_Response,
|
||||
},
|
||||
-2 => brk: {
|
||||
offset.?.* += buf.len;
|
||||
|
||||
|
||||
@@ -1353,7 +1353,8 @@ pub fn handleOnDataHeaders(
|
||||
error.ShortRead => {
|
||||
this.handleShortRead(is_ssl, incoming_data, socket, needs_move);
|
||||
},
|
||||
else => {
|
||||
error.Malformed_HTTP_Response => {
|
||||
log("Received malformed HTTP response", .{});
|
||||
this.closeAndFail(err, is_ssl, socket);
|
||||
},
|
||||
}
|
||||
|
||||
@@ -382,7 +382,7 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type {
|
||||
},
|
||||
"Sec-WebSocket-Protocol".len => {
|
||||
if (strings.eqlCaseInsensitiveASCII(header.name, "Sec-WebSocket-Protocol", false)) {
|
||||
if (this.websocket_protocol == 0 or bun.hash(header.value) != this.websocket_protocol) {
|
||||
if (this.websocket_protocol != 0 and bun.hash(header.value) != this.websocket_protocol) {
|
||||
this.terminate(ErrorCode.mismatch_client_protocol);
|
||||
return;
|
||||
}
|
||||
|
||||
129
test/regression/issue/18737-malformed-http-response.test.ts
Normal file
129
test/regression/issue/18737-malformed-http-response.test.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { createServer } from "net";
|
||||
|
||||
test("issue #18737 - malformed HTTP response handling", async () => {
|
||||
// Create a TCP server that sends malformed HTTP responses
|
||||
const server = createServer(socket => {
|
||||
socket.on("data", () => {
|
||||
// Send a malformed HTTP response that will trigger picohttp to return -1
|
||||
socket.write("INVALID_HTTP_RESPONSE_LINE\r\n\r\n");
|
||||
socket.end();
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
server.listen(0, () => resolve());
|
||||
});
|
||||
|
||||
const port = (server.address() as any).port;
|
||||
|
||||
try {
|
||||
// This should handle the malformed response gracefully
|
||||
const response = await fetch(`http://127.0.0.1:${port}/test`);
|
||||
throw new Error("Expected fetch to throw but it succeeded");
|
||||
} catch (error: any) {
|
||||
// We expect a proper error, not a crash or unhandled promise rejection
|
||||
expect(error).toBeDefined();
|
||||
expect(typeof error.message).toBe("string");
|
||||
// The error should indicate connection/parsing failure
|
||||
expect(
|
||||
error.message.includes("ECONNRESET") ||
|
||||
error.message.includes("connection") ||
|
||||
error.message.includes("network") ||
|
||||
error.code === "Malformed_HTTP_Response",
|
||||
).toBe(true);
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("issue #18737 - malformed HTTP response in WebSocket upgrade", async () => {
|
||||
// Create a server that sends malformed HTTP upgrade responses
|
||||
const server = createServer(socket => {
|
||||
socket.on("data", data => {
|
||||
const request = data.toString();
|
||||
if (request.includes("Upgrade: websocket")) {
|
||||
// Send malformed HTTP response for WebSocket upgrade
|
||||
socket.write("HTTP/1.1 101\r\n"); // Missing reason phrase
|
||||
socket.write("invalid-header-format\r\n"); // Invalid header format
|
||||
socket.write("\r\n");
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
server.listen(0, () => resolve());
|
||||
});
|
||||
|
||||
const port = (server.address() as any).port;
|
||||
|
||||
try {
|
||||
const ws = new WebSocket(`ws://127.0.0.1:${port}/test`);
|
||||
|
||||
// Should get a proper error, not crash
|
||||
await new Promise((resolve, reject) => {
|
||||
ws.onerror = (event: any) => {
|
||||
// We expect a proper error event
|
||||
expect(event).toBeDefined();
|
||||
resolve(event);
|
||||
};
|
||||
|
||||
ws.onopen = () => {
|
||||
reject(new Error("Expected WebSocket connection to fail"));
|
||||
};
|
||||
|
||||
// Timeout after 2 seconds
|
||||
setTimeout(() => {
|
||||
reject(new Error("WebSocket connection timeout"));
|
||||
}, 2000);
|
||||
});
|
||||
} catch (error) {
|
||||
// Should handle gracefully without crashing
|
||||
expect(error).toBeDefined();
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("issue #18737 - partial HTTP response handling", async () => {
|
||||
// Test the ShortRead case to ensure it's properly distinguished from malformed
|
||||
const server = createServer(socket => {
|
||||
socket.on("data", () => {
|
||||
// Send incomplete HTTP response
|
||||
socket.write("HTTP/1.1 200 OK\r\n");
|
||||
socket.write("Content-Length: 10\r\n");
|
||||
socket.write("\r\n");
|
||||
socket.write("partial"); // Only 7 bytes of 10
|
||||
// Don't end the socket - simulate connection hang
|
||||
setTimeout(() => socket.end(), 1000);
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>(resolve => {
|
||||
server.listen(0, () => resolve());
|
||||
});
|
||||
|
||||
const port = (server.address() as any).port;
|
||||
|
||||
try {
|
||||
const response = await fetch(`http://127.0.0.1:${port}/test`, {
|
||||
signal: AbortSignal.timeout(500), // Timeout quickly
|
||||
});
|
||||
|
||||
// Should be able to read partial data
|
||||
const text = await response.text();
|
||||
expect(text).toBe("partial");
|
||||
} catch (error: any) {
|
||||
// Either timeout or connection error is acceptable
|
||||
expect(
|
||||
error.name === "AbortError" ||
|
||||
error.name === "TimeoutError" ||
|
||||
error.message.includes("ECONNRESET") ||
|
||||
error.message.includes("connection") ||
|
||||
error.message.includes("timed out"),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
410
test/regression/issue/18737-websocket-nuxt-devtools.test.ts
Normal file
410
test/regression/issue/18737-websocket-nuxt-devtools.test.ts
Normal file
@@ -0,0 +1,410 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunExe, tempDirWithFiles } from "harness";
|
||||
|
||||
test("issue #18737 - WebSocket connection and malformed HTTP response with Nuxt DevTools", async () => {
|
||||
// Test case 1: Malformed HTTP response handling
|
||||
const serverCode = `
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
fetch(req) {
|
||||
// Simulate malformed HTTP response by returning invalid headers
|
||||
const url = new URL(req.url);
|
||||
if (url.pathname === '/malformed') {
|
||||
// This should trigger proper error handling without crashing
|
||||
return new Response("", {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Invalid-Header": "\\r\\n\\r\\nmalformed\\r\\n"
|
||||
}
|
||||
});
|
||||
}
|
||||
if (url.pathname === '/websocket') {
|
||||
// Test WebSocket upgrade path
|
||||
if (server.upgrade(req, {
|
||||
data: { test: true }
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
return new Response("Upgrade failed", { status: 400 });
|
||||
}
|
||||
return new Response("OK");
|
||||
},
|
||||
websocket: {
|
||||
open(ws) {
|
||||
ws.send("connection established");
|
||||
},
|
||||
message(ws, message) {
|
||||
ws.send("echo: " + message);
|
||||
},
|
||||
close(ws) {
|
||||
console.log("WebSocket closed");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(JSON.stringify({
|
||||
port: server.port,
|
||||
url: server.url.toString()
|
||||
}));
|
||||
|
||||
// Keep server alive for tests
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
server.stop();
|
||||
`;
|
||||
|
||||
const clientCode = `
|
||||
const serverInfo = JSON.parse(process.argv[2]);
|
||||
|
||||
// Test 1: Handle malformed HTTP response gracefully
|
||||
try {
|
||||
const response = await fetch(serverInfo.url + '/malformed');
|
||||
console.log("Malformed response test passed");
|
||||
} catch (error) {
|
||||
if (error.code === 'Malformed_HTTP_Response') {
|
||||
console.log("Malformed_HTTP_Response error handled correctly");
|
||||
} else {
|
||||
console.error("Unexpected error:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 2: WebSocket connection should work properly
|
||||
try {
|
||||
const ws = new WebSocket(serverInfo.url.replace('http', 'ws') + '/websocket');
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
let messageReceived = false;
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log("WebSocket connected successfully");
|
||||
ws.send("test message");
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
if (!messageReceived) {
|
||||
console.log("WebSocket message received:", event.data);
|
||||
messageReceived = true;
|
||||
ws.close();
|
||||
resolve(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error("WebSocket error:", error);
|
||||
reject(error);
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
if (messageReceived) {
|
||||
console.log("WebSocket closed properly");
|
||||
}
|
||||
};
|
||||
|
||||
// Timeout after 3 seconds
|
||||
setTimeout(() => {
|
||||
if (!messageReceived) {
|
||||
reject(new Error("WebSocket test timeout"));
|
||||
}
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
console.log("WebSocket test passed");
|
||||
} catch (error) {
|
||||
console.error("WebSocket test failed:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("All tests passed");
|
||||
`;
|
||||
|
||||
const serverDir = tempDirWithFiles("websocket-test-server", {
|
||||
"server.js": serverCode,
|
||||
});
|
||||
|
||||
const clientDir = tempDirWithFiles("websocket-test-client", {
|
||||
"client.js": clientCode,
|
||||
});
|
||||
|
||||
// Start server
|
||||
const serverProc = Bun.spawn({
|
||||
cmd: [bunExe(), "server.js"],
|
||||
cwd: serverDir,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
let serverInfo: { port: number; url: string };
|
||||
try {
|
||||
const serverOutput = await serverProc.stdout.text();
|
||||
const lines = serverOutput.trim().split("\n");
|
||||
serverInfo = JSON.parse(lines[0]);
|
||||
} catch (error) {
|
||||
const stderr = await serverProc.stderr.text();
|
||||
throw new Error(`Server failed to start: ${error}\nstderr: ${stderr}`);
|
||||
}
|
||||
|
||||
// Run client tests
|
||||
const clientProc = Bun.spawn({
|
||||
cmd: [bunExe(), "client.js", JSON.stringify(serverInfo)],
|
||||
cwd: clientDir,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [clientStdout, clientStderr, clientExitCode] = await Promise.all([
|
||||
clientProc.stdout.text(),
|
||||
clientProc.stderr.text(),
|
||||
clientProc.exited,
|
||||
]);
|
||||
|
||||
serverProc.kill();
|
||||
|
||||
if (clientExitCode !== 0) {
|
||||
console.error("Client stdout:", clientStdout);
|
||||
console.error("Client stderr:", clientStderr);
|
||||
throw new Error(`Client tests failed with exit code ${clientExitCode}`);
|
||||
}
|
||||
|
||||
expect(clientStdout).toContain("WebSocket test passed");
|
||||
expect(clientStdout).toContain("All tests passed");
|
||||
});
|
||||
|
||||
// Test for specific Nuxt DevTools scenario
|
||||
test("issue #18737 - Nuxt DevTools WebSocket HMR simulation", async () => {
|
||||
const devServerCode = `
|
||||
// Simulate Nuxt DevTools server behavior
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
fetch(req) {
|
||||
const url = new URL(req.url);
|
||||
|
||||
// DevTools WebSocket upgrade endpoint
|
||||
if (url.pathname === '/__nuxt_devtools__/client') {
|
||||
if (server.upgrade(req, {
|
||||
data: {
|
||||
type: 'devtools',
|
||||
id: Math.random().toString(36)
|
||||
}
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
return new Response("DevTools upgrade failed", { status: 400 });
|
||||
}
|
||||
|
||||
// HMR WebSocket endpoint
|
||||
if (url.pathname === '/_nuxt/hmr') {
|
||||
if (server.upgrade(req, {
|
||||
data: {
|
||||
type: 'hmr',
|
||||
id: Math.random().toString(36)
|
||||
}
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
return new Response("HMR upgrade failed", { status: 400 });
|
||||
}
|
||||
|
||||
// Regular HTTP responses that might be malformed
|
||||
if (url.pathname === '/_nuxt/dev-server-info') {
|
||||
return new Response(JSON.stringify({
|
||||
version: "3.16.2",
|
||||
devtools: true
|
||||
}), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "no-cache"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return new Response("Nuxt Dev Server");
|
||||
},
|
||||
websocket: {
|
||||
open(ws, req) {
|
||||
const data = req.data;
|
||||
console.log("WebSocket opened:", data.type);
|
||||
|
||||
if (data.type === 'devtools') {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'connected',
|
||||
payload: { status: 'ready' }
|
||||
}));
|
||||
} else if (data.type === 'hmr') {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'connected'
|
||||
}));
|
||||
|
||||
// Simulate HMR update
|
||||
setTimeout(() => {
|
||||
ws.send(JSON.stringify({
|
||||
type: 'update',
|
||||
updates: [{
|
||||
type: 'js-update',
|
||||
path: '/pages/index.vue',
|
||||
timestamp: Date.now()
|
||||
}]
|
||||
}));
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
message(ws, message, req) {
|
||||
const data = req.data;
|
||||
console.log("WebSocket message:", data.type, message);
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(message);
|
||||
if (parsed.type === 'ping') {
|
||||
ws.send(JSON.stringify({ type: 'pong' }));
|
||||
}
|
||||
} catch (e) {
|
||||
// Handle non-JSON messages
|
||||
ws.send(JSON.stringify({
|
||||
type: 'error',
|
||||
message: 'Invalid message format'
|
||||
}));
|
||||
}
|
||||
},
|
||||
close(ws, code, message, req) {
|
||||
console.log("WebSocket closed:", req.data.type, code);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(JSON.stringify({
|
||||
port: server.port,
|
||||
url: server.url.toString()
|
||||
}));
|
||||
|
||||
// Keep server alive
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
server.stop();
|
||||
`;
|
||||
|
||||
const devClientCode = `
|
||||
const serverInfo = JSON.parse(process.argv[2]);
|
||||
|
||||
let testsCompleted = 0;
|
||||
const totalTests = 2;
|
||||
|
||||
function completeTest() {
|
||||
testsCompleted++;
|
||||
if (testsCompleted === totalTests) {
|
||||
console.log("All DevTools tests passed");
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Test DevTools WebSocket connection
|
||||
const devtoolsWs = new WebSocket(serverInfo.url.replace('http', 'ws') + '/__nuxt_devtools__/client');
|
||||
|
||||
devtoolsWs.onopen = () => {
|
||||
console.log("DevTools WebSocket connected");
|
||||
devtoolsWs.send(JSON.stringify({ type: 'ping' }));
|
||||
};
|
||||
|
||||
devtoolsWs.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log("DevTools message:", data.type);
|
||||
if (data.type === 'pong') {
|
||||
console.log("DevTools test passed");
|
||||
devtoolsWs.close();
|
||||
completeTest();
|
||||
}
|
||||
};
|
||||
|
||||
devtoolsWs.onerror = (error) => {
|
||||
console.error("DevTools WebSocket error:", error);
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
// Test HMR WebSocket connection
|
||||
const hmrWs = new WebSocket(serverInfo.url.replace('http', 'ws') + '/_nuxt/hmr');
|
||||
|
||||
hmrWs.onopen = () => {
|
||||
console.log("HMR WebSocket connected");
|
||||
};
|
||||
|
||||
hmrWs.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log("HMR message:", data.type);
|
||||
if (data.type === 'update') {
|
||||
console.log("HMR test passed");
|
||||
hmrWs.close();
|
||||
completeTest();
|
||||
}
|
||||
};
|
||||
|
||||
hmrWs.onerror = (error) => {
|
||||
console.error("HMR WebSocket error:", error);
|
||||
process.exit(1);
|
||||
};
|
||||
|
||||
// Test regular HTTP endpoints
|
||||
try {
|
||||
const response = await fetch(serverInfo.url + '/_nuxt/dev-server-info');
|
||||
const info = await response.json();
|
||||
console.log("Dev server info retrieved:", info.version);
|
||||
} catch (error) {
|
||||
console.error("HTTP request failed:", error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Timeout for tests
|
||||
setTimeout(() => {
|
||||
if (testsCompleted < totalTests) {
|
||||
console.error("Tests timeout - completed:", testsCompleted, "of", totalTests);
|
||||
process.exit(1);
|
||||
}
|
||||
}, 4000);
|
||||
`;
|
||||
|
||||
const devServerDir = tempDirWithFiles("nuxt-dev-server", {
|
||||
"server.js": devServerCode,
|
||||
});
|
||||
|
||||
const devClientDir = tempDirWithFiles("nuxt-dev-client", {
|
||||
"client.js": devClientCode,
|
||||
});
|
||||
|
||||
// Start dev server
|
||||
const devServerProc = Bun.spawn({
|
||||
cmd: [bunExe(), "server.js"],
|
||||
cwd: devServerDir,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
let devServerInfo: { port: number; url: string };
|
||||
try {
|
||||
const serverOutput = await devServerProc.stdout.text();
|
||||
const lines = serverOutput.trim().split("\n");
|
||||
devServerInfo = JSON.parse(lines[0]);
|
||||
} catch (error) {
|
||||
const stderr = await devServerProc.stderr.text();
|
||||
throw new Error(`Dev server failed to start: ${error}\nstderr: ${stderr}`);
|
||||
}
|
||||
|
||||
// Run dev client tests
|
||||
const devClientProc = Bun.spawn({
|
||||
cmd: [bunExe(), "client.js", JSON.stringify(devServerInfo)],
|
||||
cwd: devClientDir,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [devClientStdout, devClientStderr, devClientExitCode] = await Promise.all([
|
||||
devClientProc.stdout.text(),
|
||||
devClientProc.stderr.text(),
|
||||
devClientProc.exited,
|
||||
]);
|
||||
|
||||
devServerProc.kill();
|
||||
|
||||
if (devClientExitCode !== 0) {
|
||||
console.error("Dev client stdout:", devClientStdout);
|
||||
console.error("Dev client stderr:", devClientStderr);
|
||||
throw new Error(`Dev client tests failed with exit code ${devClientExitCode}`);
|
||||
}
|
||||
|
||||
expect(devClientStdout).toContain("All DevTools tests passed");
|
||||
});
|
||||
204
test/regression/issue/18737-websocket-protocol-fix.test.ts
Normal file
204
test/regression/issue/18737-websocket-protocol-fix.test.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { test } from "bun:test";
|
||||
|
||||
test("issue #18737 - WebSocket protocol validation fix", async () => {
|
||||
// Test case 1: WebSocket without specific protocol should accept server protocol
|
||||
const server1 = Bun.serve({
|
||||
port: 0,
|
||||
fetch(req, server) {
|
||||
if (
|
||||
server.upgrade(req, {
|
||||
data: { test: "no-protocol" },
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
return new Response("Upgrade failed", { status: 400 });
|
||||
},
|
||||
websocket: {
|
||||
open(ws, req) {
|
||||
ws.send("connected-no-protocol");
|
||||
},
|
||||
message(ws, message, req) {
|
||||
ws.send("echo: " + message);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// Client doesn't specify a protocol, server may send one - this should work
|
||||
const ws1 = new WebSocket(`ws://localhost:${server1.port}/test`);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
ws1.onopen = () => {
|
||||
ws1.send("test");
|
||||
};
|
||||
|
||||
ws1.onmessage = event => {
|
||||
if (event.data === "connected-no-protocol") {
|
||||
// Connection established successfully
|
||||
resolve();
|
||||
} else if (event.data === "echo: test") {
|
||||
ws1.close();
|
||||
}
|
||||
};
|
||||
|
||||
ws1.onerror = error => {
|
||||
reject(new Error(`WebSocket error: ${error}`));
|
||||
};
|
||||
|
||||
ws1.onclose = () => {
|
||||
resolve();
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
reject(new Error("WebSocket test timeout"));
|
||||
}, 3000);
|
||||
});
|
||||
} finally {
|
||||
server1.stop();
|
||||
}
|
||||
|
||||
// Test case 2: WebSocket with specific protocol should match server protocol
|
||||
const server2 = Bun.serve({
|
||||
port: 0,
|
||||
fetch(req, server) {
|
||||
const protocol = req.headers.get("Sec-WebSocket-Protocol");
|
||||
if (
|
||||
server.upgrade(req, {
|
||||
data: { protocol },
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
return new Response("Upgrade failed", { status: 400 });
|
||||
},
|
||||
websocket: {
|
||||
open(ws, req) {
|
||||
ws.send("connected-with-protocol");
|
||||
},
|
||||
message(ws, message, req) {
|
||||
ws.send("echo: " + message);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// Client specifies a protocol - this should also work
|
||||
const ws2 = new WebSocket(`ws://localhost:${server2.port}/test`, ["echo-protocol"]);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let messageReceived = false;
|
||||
|
||||
ws2.onopen = () => {
|
||||
ws2.send("test");
|
||||
};
|
||||
|
||||
ws2.onmessage = event => {
|
||||
if (event.data === "connected-with-protocol" && !messageReceived) {
|
||||
messageReceived = true;
|
||||
// Connection established successfully
|
||||
} else if (event.data === "echo: test") {
|
||||
ws2.close();
|
||||
}
|
||||
};
|
||||
|
||||
ws2.onerror = error => {
|
||||
reject(new Error(`WebSocket with protocol error: ${error}`));
|
||||
};
|
||||
|
||||
ws2.onclose = () => {
|
||||
if (messageReceived) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error("WebSocket closed without receiving expected messages"));
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
reject(new Error("WebSocket protocol test timeout"));
|
||||
}, 3000);
|
||||
});
|
||||
} finally {
|
||||
server2.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test("issue #18737 - WebSocket connection resilience", async () => {
|
||||
// Test that WebSocket connections handle various server responses gracefully
|
||||
let connectionAttempts = 0;
|
||||
const maxAttempts = 3;
|
||||
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
fetch(req, server) {
|
||||
connectionAttempts++;
|
||||
|
||||
// Simulate server behavior that might cause issues
|
||||
if (connectionAttempts === 1) {
|
||||
// First attempt - simulate server that doesn't handle WebSocket properly
|
||||
return new Response("Not a WebSocket server", { status: 400 });
|
||||
}
|
||||
|
||||
if (connectionAttempts === 2) {
|
||||
// Second attempt - simulate successful upgrade
|
||||
if (server.upgrade(req, { data: { attempt: connectionAttempts } })) {
|
||||
return;
|
||||
}
|
||||
return new Response("Upgrade failed", { status: 400 });
|
||||
}
|
||||
|
||||
return new Response("OK");
|
||||
},
|
||||
websocket: {
|
||||
open(ws, req) {
|
||||
const attempt = req.data?.attempt || connectionAttempts;
|
||||
ws.send(`connected-attempt-${attempt}`);
|
||||
},
|
||||
message(ws, message, req) {
|
||||
const attempt = req.data?.attempt || connectionAttempts;
|
||||
ws.send(`echo-${attempt}: ${message}`);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// First connection attempt should fail gracefully
|
||||
try {
|
||||
const ws1 = new WebSocket(`ws://localhost:${server.port}/test`);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
ws1.onerror = () => resolve(); // Expected to fail
|
||||
ws1.onopen = () => reject(new Error("Expected first connection to fail"));
|
||||
setTimeout(() => resolve(), 1000); // Timeout is OK
|
||||
});
|
||||
} catch (error) {
|
||||
// This is expected to fail
|
||||
}
|
||||
|
||||
// Second connection attempt should succeed
|
||||
const ws2 = new WebSocket(`ws://localhost:${server.port}/test`);
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
ws2.onopen = () => {
|
||||
ws2.send("test");
|
||||
};
|
||||
|
||||
ws2.onmessage = event => {
|
||||
if (event.data === "connected-attempt-2") {
|
||||
// Good, connection established
|
||||
} else if (event.data === "echo-2: test") {
|
||||
ws2.close();
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
ws2.onerror = error => {
|
||||
reject(new Error(`Second WebSocket connection failed: ${error}`));
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
reject(new Error("WebSocket resilience test timeout"));
|
||||
}, 3000);
|
||||
});
|
||||
} finally {
|
||||
server.stop();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,129 @@
|
||||
import { test } from "bun:test";
|
||||
|
||||
test("issue #18737 - WebSocket protocol validation fix", async () => {
|
||||
// Test the specific bug: client without protocol should accept server with protocol
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
fetch(req, server) {
|
||||
if (server.upgrade(req)) {
|
||||
return;
|
||||
}
|
||||
return new Response("Upgrade failed", { status: 400 });
|
||||
},
|
||||
websocket: {
|
||||
open(ws) {
|
||||
ws.send("connected");
|
||||
},
|
||||
message(ws, message) {
|
||||
ws.send("echo: " + message);
|
||||
ws.close();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// Client doesn't specify a protocol - this should work even if server sends one
|
||||
const ws = new WebSocket(`ws://localhost:${server.port}/test`);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let connected = false;
|
||||
|
||||
ws.onopen = () => {
|
||||
connected = true;
|
||||
ws.send("test");
|
||||
};
|
||||
|
||||
ws.onmessage = event => {
|
||||
if (event.data === "connected") {
|
||||
// Good, connection established
|
||||
} else if (event.data === "echo: test") {
|
||||
// Echo received, connection working
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
if (connected) {
|
||||
resolve(); // Success - connection worked without protocol mismatch error
|
||||
} else {
|
||||
reject(new Error("WebSocket closed before connecting"));
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = error => {
|
||||
reject(new Error(`WebSocket error: ${error}`));
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
reject(new Error("WebSocket test timeout"));
|
||||
}, 3000);
|
||||
});
|
||||
} finally {
|
||||
server.stop();
|
||||
}
|
||||
});
|
||||
|
||||
test("issue #18737 - WebSocket with specific protocol", async () => {
|
||||
// Test that specific protocol matching still works
|
||||
const server = Bun.serve({
|
||||
port: 0,
|
||||
fetch(req, server) {
|
||||
const protocol = req.headers.get("sec-websocket-protocol");
|
||||
if (protocol?.includes("echo-protocol")) {
|
||||
// Accept the echo-protocol
|
||||
if (server.upgrade(req)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return new Response("Protocol not supported", { status: 400 });
|
||||
},
|
||||
websocket: {
|
||||
open(ws) {
|
||||
ws.send("protocol-connected");
|
||||
},
|
||||
message(ws, message) {
|
||||
ws.send("protocol-echo: " + message);
|
||||
ws.close();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
// Client specifies a protocol - this should also work
|
||||
const ws = new WebSocket(`ws://localhost:${server.port}/test`, ["echo-protocol"]);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let connected = false;
|
||||
|
||||
ws.onopen = () => {
|
||||
connected = true;
|
||||
ws.send("test");
|
||||
};
|
||||
|
||||
ws.onmessage = event => {
|
||||
if (event.data === "protocol-connected") {
|
||||
// Good, connection established
|
||||
} else if (event.data === "protocol-echo: test") {
|
||||
// Echo received, connection working
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
if (connected) {
|
||||
resolve(); // Success
|
||||
} else {
|
||||
reject(new Error("WebSocket closed before connecting"));
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = error => {
|
||||
reject(new Error(`WebSocket with protocol error: ${error}`));
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
reject(new Error("WebSocket protocol test timeout"));
|
||||
}, 3000);
|
||||
});
|
||||
} finally {
|
||||
server.stop();
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user