Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions src/runtime/server/WebSocketServerContext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ pub(crate) fn on_create(
"websocket expects maxPayloadLength to be an integer"
)));
}
server.max_payload_length = value.to_int64().max(0) as u32;
server.max_payload_length = value.to_u32();
}
}

Expand All @@ -321,12 +321,14 @@ pub(crate) fn on_create(
)));
}

let mut idle_timeout: u16 = value.to_int64().max(0) as u16;
let idle_timeout = value.to_int64().max(0);
if idle_timeout > 960 {
return Err(global_object.throw_invalid_arguments(format_args!(
"websocket expects idleTimeout to be 960 or less"
)));
Comment thread
robobun marked this conversation as resolved.
Resolved
} else if idle_timeout > 0 {
}
let mut idle_timeout = idle_timeout as u16;
if idle_timeout > 0 {
// uws does not allow idleTimeout to be between (0, 8),
// since its timer is not that accurate, therefore round up.
idle_timeout = idle_timeout.max(8);
Expand All @@ -343,7 +345,7 @@ pub(crate) fn on_create(
)));
}

server.backpressure_limit = value.to_int64().max(0) as u32;
server.backpressure_limit = value.to_u32();
}
}

Expand Down
104 changes: 104 additions & 0 deletions test/js/bun/http/bun-serve-args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,110 @@ describe("Bun.serve websocket options", () => {
});
server.stop();
});

test("idleTimeout above 960 throws instead of wrapping at 2^16", () => {
// 65536 and 65566 used to wrap to 0 and 30 as u16 and be accepted.
for (const idleTimeout of [961, 65536, 65566, 2 ** 32 + 30]) {
expect(() =>
serve({
port: 0,
websocket: { message() {}, idleTimeout },
fetch() {
return new Response("ok");
},
}),
).toThrow("websocket expects idleTimeout to be 960 or less");
}

// The boundary value is still accepted.
using server = serve({
port: 0,
websocket: { message() {}, idleTimeout: 960 },
fetch() {
return new Response("ok");
},
});
server.stop();
});

test("maxPayloadLength above 2^32 clamps instead of wrapping to a tiny limit", async () => {
// 2^32 + 16 used to truncate to 16, so every message over 16 bytes
// killed the connection.
using server = serve({
port: 0,
hostname: "127.0.0.1",
websocket: {
maxPayloadLength: 2 ** 32 + 16,
message(ws, message) {
ws.send(`echo:${message.length}`);
},
},
fetch(req, server) {
if (server.upgrade(req)) {
return;
}
return new Response("Not a websocket", { status: 400 });
},
});

const { promise, resolve, reject } = Promise.withResolvers<string>();
const ws = new WebSocket(`ws://127.0.0.1:${server.port}/`);
ws.onopen = () => ws.send(Buffer.alloc(100, "x").toString());
ws.onmessage = e => resolve(e.data as string);
ws.onerror = () => reject(new Error("websocket errored before echo"));
ws.onclose = e => reject(new Error(`closed before echo: code ${e.code}`));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
try {
expect(await promise).toBe("echo:100");
} finally {
ws.onerror = null;
ws.onclose = null;
ws.close();
server.stop(true);
}
});

test("backpressureLimit above 2^32 clamps instead of wrapping to a tiny limit", async () => {
// 2^32 + 1 used to truncate to 1 byte, so once a send left anything
// buffered, the next send was dropped (returned 0).
const { promise, resolve, reject } = Promise.withResolvers<number[]>();
using server = serve({
port: 0,
hostname: "127.0.0.1",
websocket: {
backpressureLimit: 2 ** 32 + 1,
open(ws) {
const data = new Uint8Array(4 * 1024 * 1024);
const results: number[] = [];
for (let i = 0; i < 6; i++) {
results.push(ws.send(data));
}
resolve(results);
},
message() {},
},
fetch(req, server) {
if (server.upgrade(req)) {
return;
}
return new Response("Not a websocket", { status: 400 });
},
});

const ws = new WebSocket(`ws://127.0.0.1:${server.port}/`);
ws.onerror = () => reject(new Error("websocket errored before open"));
ws.onclose = e => reject(new Error(`closed before open: code ${e.code}`));
try {
const results = await promise;
// 0 means dropped for exceeding backpressureLimit; a ~4 GiB limit must
// never drop 24 MB of queued sends (-1 backpressure and >0 sent are fine).
expect(results.filter(r => r === 0)).toEqual([]);
} finally {
ws.onerror = null;
ws.onclose = null;
ws.close();
server.stop(true);
}
});
});

describe("Bun.serve development options", () => {
Expand Down
Loading