mirror of
https://github.com/oven-sh/bun
synced 2026-02-16 22:01:47 +00:00
Compare commits
13 Commits
claude/fix
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1eb4070bb | ||
|
|
3b8eb7426f | ||
|
|
1400e05e11 | ||
|
|
c8e3a91602 | ||
|
|
2e5f7f10ae | ||
|
|
559c95ee2c | ||
|
|
262f8863cb | ||
|
|
05f5ea0070 | ||
|
|
46ce975175 | ||
|
|
fa4822f8b8 | ||
|
|
8881e671d4 | ||
|
|
97d55411de | ||
|
|
f247277375 |
5
.github/workflows/format.yml
vendored
5
.github/workflows/format.yml
vendored
@@ -8,10 +8,8 @@ on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
merge_group:
|
||||
push:
|
||||
branches: ["main"]
|
||||
env:
|
||||
BUN_VERSION: "1.2.11"
|
||||
BUN_VERSION: "1.2.20"
|
||||
LLVM_VERSION: "19.1.7"
|
||||
LLVM_VERSION_MAJOR: "19"
|
||||
|
||||
@@ -37,6 +35,7 @@ jobs:
|
||||
- name: Setup Dependencies
|
||||
run: |
|
||||
bun install
|
||||
bun scripts/glob-sources.mjs
|
||||
- name: Format Code
|
||||
run: |
|
||||
# Start prettier in background with prefixed output
|
||||
|
||||
41
.github/workflows/glob-sources.yml
vendored
41
.github/workflows/glob-sources.yml
vendored
@@ -1,41 +0,0 @@
|
||||
name: Glob Sources
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
BUN_VERSION: "1.2.11"
|
||||
|
||||
jobs:
|
||||
glob-sources:
|
||||
name: Glob Sources
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config --global core.autocrlf true
|
||||
git config --global core.ignorecase true
|
||||
git config --global core.precomposeUnicode true
|
||||
- name: Setup Bun
|
||||
uses: ./.github/actions/setup-bun
|
||||
with:
|
||||
bun-version: ${{ env.BUN_VERSION }}
|
||||
- name: Setup Dependencies
|
||||
run: |
|
||||
bun install
|
||||
- name: Glob sources
|
||||
run: bun scripts/glob-sources.mjs
|
||||
- name: Commit
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: "`bun scripts/glob-sources.mjs`"
|
||||
|
||||
41
.github/workflows/labeled.yml
vendored
41
.github/workflows/labeled.yml
vendored
@@ -5,6 +5,8 @@ env:
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
pull_request_target:
|
||||
types: [labeled, opened, reopened, synchronize, unlabeled]
|
||||
|
||||
jobs:
|
||||
# on-bug:
|
||||
@@ -43,9 +45,46 @@ jobs:
|
||||
# token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# issue-number: ${{ github.event.issue.number }}
|
||||
# labels: ${{ steps.add-labels.outputs.labels }}
|
||||
on-slop:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request_target' && contains(github.event.pull_request.labels.*.name, 'slop')
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
- name: Update PR title and body for slop and close
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const pr = await github.rest.pulls.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.issue.number
|
||||
});
|
||||
|
||||
await github.rest.pulls.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.issue.number,
|
||||
title: 'ai slop',
|
||||
body: 'This PR has been marked as AI slop and the description has been updated to avoid confusion or misleading reviewers.\n\nMany AI PRs are fine, but sometimes they submit a PR too early, fail to test if the problem is real, fail to reproduce the problem, or fail to test that the problem is fixed. If you think this PR is not AI slop, please leave a comment.',
|
||||
state: 'closed'
|
||||
});
|
||||
|
||||
// Delete the branch if it's from a fork or if it's not a protected branch
|
||||
try {
|
||||
await github.rest.git.deleteRef({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
ref: `heads/${pr.data.head.ref}`
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Could not delete branch:', error.message);
|
||||
}
|
||||
on-labeled:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.label.name == 'crash' || github.event.label.name == 'needs repro'
|
||||
if: github.event_name == 'issues' && (github.event.label.name == 'crash' || github.event.label.name == 'needs repro')
|
||||
permissions:
|
||||
issues: write
|
||||
steps:
|
||||
|
||||
@@ -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