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
2 changes: 1 addition & 1 deletion src/js/internal/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ function hasServerResponseFinished(self, chunk, callback) {
if (finished) {
err = $ERR_STREAM_WRITE_AFTER_END();
} else if (destroyed) {
err = $ERR_STREAM_DESTROYED("Stream is destroyed");
err = $ERR_STREAM_DESTROYED("write");
}

if (!destroyed) {
Expand Down
4 changes: 2 additions & 2 deletions src/js/node/_http_server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2114,7 +2114,7 @@ function _writeHead(statusCode, reason, obj, response) {
// header fields, regardless of the header fields present in the
// message, and thus cannot contain a message body or 'trailers'.
if (hasInvalidTrailer(response)) {
throw $ERR_HTTP_TRAILER_INVALID("Trailers are invalid with this transfer encoding");
throw $ERR_HTTP_TRAILER_INVALID();
}
// Headers in obj should override previous headers but still
// allow explicit duplicates. To do so, we first remove any
Expand Down Expand Up @@ -2144,7 +2144,7 @@ function _writeHead(statusCode, reason, obj, response) {
// The message is not chunk-framed, so `Trailer` is the offending header; drop it
// so a caller that swallows the throw cannot put it on the wire.
response.removeHeader("trailer");
throw $ERR_HTTP_TRAILER_INVALID("Trailers are invalid with this transfer encoding");
throw $ERR_HTTP_TRAILER_INVALID();
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/js/node/fs.promises.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1148,7 +1148,7 @@ function asyncWrap(fn: any, name: string) {

if (bytesWritten === 0) {
if (++retries > 5) {
throw $ERR_OPERATION_FAILED("Operation failed: write failed after retries");
throw $ERR_OPERATION_FAILED("write failed after retries");
}
} else {
retries = 0;
Expand Down Expand Up @@ -1192,7 +1192,7 @@ function asyncWrap(fn: any, name: string) {
// Retry the writev as-is on a zero-byte write (up to 5 times)
// instead of degrading to the concat fallback below.
if (++retries > 5) {
throw $ERR_OPERATION_FAILED("Operation failed: writev failed after retries");
throw $ERR_OPERATION_FAILED("writev failed after retries");
}
continue;
}
Expand Down Expand Up @@ -1229,7 +1229,7 @@ function asyncWrap(fn: any, name: string) {
const bytesWritten = fsSync.writeSync(fd, buf, offset, length, position >= 0 ? position : null) || 0;
if (bytesWritten === 0) {
if (++retries > 5) {
throw $ERR_OPERATION_FAILED("Operation failed: write failed after retries");
throw $ERR_OPERATION_FAILED("write failed after retries");
}
} else {
retries = 0;
Expand Down
32 changes: 31 additions & 1 deletion src/jsc/bindings/ErrorCode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2391,6 +2391,25 @@ JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_HTTP_HEADERS_SENT, message));
}

case Bun::ErrorCode::ERR_HTTP_CONTENT_LENGTH_MISMATCH: {
auto arg0 = callFrame->argument(1);
auto str0 = arg0.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
auto arg1 = callFrame->argument(2);
auto str1 = arg1.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
auto message = makeString("Response body's content-length of "_s, str0, " byte(s) does not match the content-length of "_s, str1, " byte(s) set in header"_s);
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_HTTP_CONTENT_LENGTH_MISMATCH, message));
Comment thread
robobun marked this conversation as resolved.
}

case Bun::ErrorCode::ERR_HTTP2_UNSUPPORTED_PROTOCOL: {
auto arg0 = callFrame->argument(1);
auto str0 = arg0.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, {});
auto message = makeString("protocol \""_s, str0, "\" is unsupported."_s);
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_HTTP2_UNSUPPORTED_PROTOCOL, message));
}

case Bun::ErrorCode::ERR_UNESCAPED_CHARACTERS: {
auto arg0 = callFrame->argument(1);
auto str0 = arg0.toWTFString(globalObject);
Expand Down Expand Up @@ -2670,6 +2689,10 @@ JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_HTTP2_GOAWAY_SESSION, "New streams cannot be created after receiving a GOAWAY"_s));
case ErrorCode::ERR_HTTP_BODY_NOT_ALLOWED:
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_HTTP_BODY_NOT_ALLOWED, "Adding content for this request method or response status is not allowed."_s));
case ErrorCode::ERR_HTTP_TRAILER_INVALID:
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_HTTP_TRAILER_INVALID, "Trailers are invalid with this transfer encoding"_s));
Comment thread
robobun marked this conversation as resolved.
case ErrorCode::ERR_SCRIPT_EXECUTION_INTERRUPTED:
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_SCRIPT_EXECUTION_INTERRUPTED, "Script execution was interrupted by `SIGINT`"_s));
case ErrorCode::ERR_HTTP_SOCKET_ASSIGNED:
return JSC::JSValue::encode(createError(globalObject, ErrorCode::ERR_HTTP_SOCKET_ASSIGNED, "Socket already assigned"_s));
case ErrorCode::ERR_STREAM_RELEASE_LOCK:
Expand Down Expand Up @@ -2708,7 +2731,14 @@ JSC_DEFINE_HOST_FUNCTION(Bun::jsFunctionMakeErrorWithCode, (JSC::JSGlobalObject
}
}

auto&& message = callFrame->argument(1).toWTFString(globalObject);
auto messageValue = callFrame->argument(1);
#if ASSERT_ENABLED
if (!messageValue.isString()) {
JSC::throwTypeError(globalObject, scope, makeString("$ERR_ code "_s, static_cast<int>(error), " has no message template; first argument must be the full message string"_s));
RELEASE_AND_RETURN(scope, {});
}
#endif
Comment thread
robobun marked this conversation as resolved.
auto&& message = messageValue.toWTFString(globalObject);
RETURN_IF_EXCEPTION(scope, {});

return JSC::JSValue::encode(createError(globalObject, error, message));
Expand Down
30 changes: 24 additions & 6 deletions src/runtime/server/NodeHTTPResponse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,24 @@ fn err_throw<T>(global: &JSGlobalObject, code: ErrorCode, msg: &'static str) ->
Err(err_throw_cold(global, code, msg))
}

#[cold]
#[inline(never)]
fn err_throw_content_length_mismatch<T>(
global: &JSGlobalObject,
actual: u64,
expected: u64,
) -> JsResult<T> {
Err(global
.err(
ErrorCode::ERR_HTTP_CONTENT_LENGTH_MISMATCH,
format_args!(
"Response body's content-length of {} byte(s) does not match the content-length of {} byte(s) set in header",
actual, expected
),
)
.throw())
}

/// AnyResponse `is_ssl()` shim (upstream lacks this accessor).
#[inline]
fn any_response_is_ssl(r: &uws::AnyResponse) -> bool {
Expand Down Expand Up @@ -1989,17 +2007,17 @@ impl NodeHTTPResponse {

if IS_END {
if bytes_written as u64 != content_length {
return err_throw(
return err_throw_content_length_mismatch(
global_object,
ErrorCode::ERR_HTTP_CONTENT_LENGTH_MISMATCH,
"Content-Length mismatch",
bytes_written as u64,
content_length,
);
}
} else if bytes_written as u64 > content_length {
return err_throw(
return err_throw_content_length_mismatch(
global_object,
ErrorCode::ERR_HTTP_CONTENT_LENGTH_MISMATCH,
"Content-Length mismatch",
bytes_written as u64,
content_length,
);
}
self.bytes_written.set(bytes_written);
Expand Down
111 changes: 111 additions & 0 deletions test/js/node/node-error-messages.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { expect, test } from "bun:test";
import { once } from "node:events";
import http from "node:http";
import http2 from "node:http2";
import net from "node:net";

// Node.js error codes are produced in C++ (ErrorCode.cpp jsFunctionMakeErrorWithCode).
// Codes with no message template fall through to "use the first JS argument verbatim
// as .message", so a call site that passes template args instead of the full sentence
// yields a broken message, and a call site that passes the full sentence into a code
// that *is* templated gets the template wrapper applied twice. These tests pin the
// messages to Node.js's format for the codes that were observably wrong.
Comment thread
robobun marked this conversation as resolved.

test("ERR_HTTP2_UNSUPPORTED_PROTOCOL message matches Node.js", () => {
let err: any;
try {
http2.connect("gopher://127.0.0.1:1");
} catch (e) {
err = e;
}
expect(err?.code).toBe("ERR_HTTP2_UNSUPPORTED_PROTOCOL");
expect(err?.message).toBe('protocol "gopher:" is unsupported.');
});

test("ERR_HTTP_TRAILER_INVALID message matches Node.js", () => {
const req = http.request({ port: 1, method: "POST", createConnection: () => new net.Socket() });
req.on("error", () => {});
req.setHeader("Content-Length", "5");
req.setHeader("Trailer", "X-Foo");
let err: any;
try {
req.flushHeaders();
} catch (e) {
err = e;
}
req.destroy();
expect({ code: err?.code, message: err?.message }).toEqual({
code: "ERR_HTTP_TRAILER_INVALID",
message: "Trailers are invalid with this transfer encoding",
});
});

test("ERR_HTTP_CONTENT_LENGTH_MISMATCH message matches Node.js", () => {
const req = http.request({ port: 1, method: "POST", createConnection: () => new net.Socket() });
req.on("error", () => {});
req.strictContentLength = true;
req.setHeader("Content-Length", "5");
req.flushHeaders();
let err: any;
try {
req.write("hello world");
} catch (e) {
err = e;
}
req.destroy();
expect({ code: err?.code, message: err?.message }).toEqual({
code: "ERR_HTTP_CONTENT_LENGTH_MISMATCH",
message:
"Response body's content-length of 11 byte(s) does not match the content-length of 5 byte(s) set in header",
});
});

test("ERR_HTTP_CONTENT_LENGTH_MISMATCH message from http ServerResponse matches Node.js", async () => {
const { promise, resolve } = Promise.withResolvers<any>();
const server = http.createServer((req, res) => {
res.strictContentLength = true;
res.setHeader("Content-Length", "5");
try {
res.end("hello world");
resolve(undefined);
} catch (e) {
resolve(e);
}
try {
res.destroy();
} catch {}
});
server.listen(0);
await once(server, "listening");
const sock = net.connect((server.address() as net.AddressInfo).port);
sock.on("error", () => {});
sock.write("GET / HTTP/1.1\r\nHost: x\r\n\r\n");
const err = await promise;
sock.destroy();
await new Promise<void>(r => server.close(() => r()));
expect({ code: err?.code, message: err?.message }).toEqual({
code: "ERR_HTTP_CONTENT_LENGTH_MISMATCH",
message:
"Response body's content-length of 11 byte(s) does not match the content-length of 5 byte(s) set in header",
});
});

test("ERR_STREAM_DESTROYED message from http ServerResponse matches Node.js", async () => {
const { promise, resolve } = Promise.withResolvers<any>();
const server = http.createServer((req, res) => {
res.destroy();
res.write("x", resolve);
});
server.listen(0);
await once(server, "listening");
const sock = net.connect((server.address() as net.AddressInfo).port);
sock.on("error", () => {});
sock.write("GET / HTTP/1.1\r\nHost: x\r\n\r\n");
const err = await promise;
sock.destroy();
await new Promise<void>(r => server.close(() => r()));
expect({ code: err?.code, message: err?.message }).toEqual({
code: "ERR_STREAM_DESTROYED",
message: "Cannot call write after a stream was destroyed",
});
});
Loading