Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
71 changes: 45 additions & 26 deletions src/runtime/socket/Handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,38 @@
generated: &GeneratedSocketConfigHandlers,
is_server: bool,
) -> JsResult<Handlers> {
// Validate the config before constructing a `Handlers`: an invalid
// handler must return an error while there is nothing to clean up.
// A constructed-but-not-yet-`protect()`ed `Handlers` whose `Drop` runs
// `unprotect()` would trip `debug_assert!(protection_count > 0)` (debug
// abort) and unprotect callbacks `protect()` never rooted (release).
macro_rules! validate_callback {
($field:ident, $name:literal) => {{
let value = generated.$field;
if !value.is_undefined_or_null() && !value.is_callable() {
return Err(global_object.throw_invalid_arguments(format_args!(
"Expected \"{}\" callback to be a function",
$name
)));
}
}};
}
validate_callback!(on_open, "onOpen");
validate_callback!(on_close, "onClose");
validate_callback!(on_data, "onData");
validate_callback!(on_writable, "onWritable");
validate_callback!(on_timeout, "onTimeout");
validate_callback!(on_connect_error, "onConnectError");
validate_callback!(on_end, "onEnd");
validate_callback!(on_error, "onError");
validate_callback!(on_handshake, "onHandshake");

Check notice on line 313 in src/runtime/socket/Handlers.rs

View check run for this annotation

Claude / Claude Code Review

Error messages use internal handler names instead of user-facing ones

Nit (pre-existing, not introduced here): these error messages use the internal handler names (`"onOpen"`, `"onWritable"`, …) rather than the user-facing keys defined in `SocketConfig.bindv2.ts` (`"open"`, `"drain"`, …). So `socket: { drain: 123 }` produces `Expected "onWritable" callback to be a function` — a name the user never wrote. Since these literals were re-typed into the new `validate_callback!` macro anyway, it might be worth swapping them to the public names in passing; non-blocking ei
Comment thread
robobun marked this conversation as resolved.

if !generated.on_data.is_callable() && !generated.on_writable.is_callable() {
return Err(global_object.throw_invalid_arguments(format_args!(
"Expected at least \"data\" or \"drain\" callback"
)));
}

let mut result = Handlers {
on_open: JSValue::ZERO,
on_close: JSValue::ZERO,
Expand Down Expand Up @@ -316,36 +348,23 @@
protection_count: 0,
};

// inline for (callback_fields) |field| { ... @field(generated, field) ... }
// Callbacks are validated above; assign the ones that are present.
macro_rules! assign_callback {
($field:ident, $name:literal) => {{
let value = generated.$field;
if value.is_undefined_or_null() {
} else if !value.is_callable() {
return Err(global_object.throw_invalid_arguments(format_args!(
"Expected \"{}\" callback to be a function",
$name
)));
} else {
result.$field = value;
($field:ident) => {{
if generated.$field.is_callable() {
result.$field = generated.$field;
}
}};
}
assign_callback!(on_open, "onOpen");
assign_callback!(on_close, "onClose");
assign_callback!(on_data, "onData");
assign_callback!(on_writable, "onWritable");
assign_callback!(on_timeout, "onTimeout");
assign_callback!(on_connect_error, "onConnectError");
assign_callback!(on_end, "onEnd");
assign_callback!(on_error, "onError");
assign_callback!(on_handshake, "onHandshake");

if result.on_data.is_empty() && result.on_writable.is_empty() {
return Err(global_object.throw_invalid_arguments(format_args!(
"Expected at least \"data\" or \"drain\" callback"
)));
}
assign_callback!(on_open);
assign_callback!(on_close);
assign_callback!(on_data);
assign_callback!(on_writable);
assign_callback!(on_timeout);
assign_callback!(on_connect_error);
assign_callback!(on_end);
assign_callback!(on_error);
assign_callback!(on_handshake);
result.with_async_context_if_needed(global_object);
result.protect();
Ok(result)
Expand Down
26 changes: 26 additions & 0 deletions test/js/bun/net/socket.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1558,3 +1558,29 @@ it("node:net connect() reusing a server-accepted handle keeps the listener's han
expect(exitCode).toBe(0);
void stderr;
});

it("Bun.listen with an invalid socket handler throws ERR_INVALID_ARG_TYPE instead of aborting", async () => {
// Regression: a non-function handler used to abort the debug process (Drop ran
// unprotect() on a Handlers that protect() never rooted). It must throw a
// catchable error. Run in a subprocess so a regression (abort) shows up as a
// non-zero exit rather than killing the test runner.
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`try {
Bun.listen({ port: 0, hostname: "127.0.0.1", socket: { data: 123 } });
process.exit(2); // did not throw
} catch (e) {
process.stdout.write(String(e.code));
process.exit(0);
}`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, , exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toBe("ERR_INVALID_ARG_TYPE");
expect(exitCode).toBe(0);
Comment thread
robobun marked this conversation as resolved.
});
Loading