Skip to content
Open
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
7 changes: 7 additions & 0 deletions src/jsc/SystemError.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::{JSGlobalObject, JSPromise, JSValue};
#[repr(C)]
#[derive(Clone)]
pub struct SystemError {
/// [`SystemError::NO_ERRNO`] = the JS error gets `errno: undefined`
pub errno: c_int,
/// label for errno
pub code: OwnedString,
Expand Down Expand Up @@ -80,6 +81,12 @@ unsafe extern "C" {
}

impl SystemError {
/// `errno` for an error that has no numeric system error code, only a
/// string `code`. Node reports such errors (a c-ares resolver failure, for
/// example) with an own `errno` property whose value is `undefined`, and
/// the C++ side (`systemErrorToErrorInstance`) emits exactly that.
Comment thread
robobun marked this conversation as resolved.
pub const NO_ERRNO: c_int = c_int::MIN;

/// Converts to a JS `Error`, consuming `self`. C++ only borrows the string
/// fields; `Drop` releases them when `self` goes out of scope. `.clone()`
/// first when two `Error`s are genuinely wanted.
Expand Down
16 changes: 13 additions & 3 deletions src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2569,6 +2569,15 @@ JSC::EncodedJSValue JSGlobalObject__createOutOfMemoryError(JSC::JSGlobalObject*
return JSValue::encode(exception);
}

// `SystemError::NO_ERRNO` on the Rust side: the error has a string `code` but
// no numeric errno, and Node reports those with `errno: undefined`.
Comment thread
robobun marked this conversation as resolved.
static JSC::JSValue systemErrorErrnoValue(const SystemError& err)
{
if (err.errno_ == std::numeric_limits<int>::min())
return JSC::jsUndefined();
return JSC::jsNumber(err.errno_);
}

static JSC::EncodedJSValue systemErrorToErrorInstance(const SystemError* arg0, JSC::JSGlobalObject* globalObject, JSC::ErrorType errorType)
{
SystemError err = *arg0;
Expand Down Expand Up @@ -2637,7 +2646,7 @@ static JSC::EncodedJSValue systemErrorToErrorInstance(const SystemError* arg0, J
}
}

result->putDirect(vm, names.errnoPublicName(), jsNumber(err.errno_), JSC::PropertyAttribute::DontDelete | 0);
result->putDirect(vm, names.errnoPublicName(), systemErrorErrnoValue(err), JSC::PropertyAttribute::DontDelete | 0);

return JSC::JSValue::encode(result);
}
Expand Down Expand Up @@ -2683,8 +2692,9 @@ JSC::EncodedJSValue SystemError__toErrorInstanceWithInfoObject(const SystemError
info->putDirect(vm, clientData->builtinNames().codePublicName(), jsString(vm, codeString), JSC::PropertyAttribute::DontDelete | 0);
info->putDirect(vm, vm.propertyNames->message, jsString(vm, messageString), JSC::PropertyAttribute::DontDelete | 0);

info->putDirect(vm, clientData->builtinNames().errnoPublicName(), jsNumber(err.errno_), JSC::PropertyAttribute::DontDelete | 0);
result->putDirect(vm, clientData->builtinNames().errnoPublicName(), jsNumber(err.errno_), JSC::PropertyAttribute::DontDelete | 0);
JSC::JSValue errnoValue = systemErrorErrnoValue(err);
info->putDirect(vm, clientData->builtinNames().errnoPublicName(), errnoValue, JSC::PropertyAttribute::DontDelete | 0);
result->putDirect(vm, clientData->builtinNames().errnoPublicName(), errnoValue, JSC::PropertyAttribute::DontDelete | 0);

return JSC::JSValue::encode(result);
}
Expand Down
1 change: 1 addition & 0 deletions src/jsc/bindings/headers-handwritten.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ typedef struct ErrorableResolvedSource {
} ErrorableResolvedSource;

typedef struct SystemError {
/// MinInt (`SystemError::NO_ERRNO` in Rust) = the error gets `errno: undefined`
int errno_;
BunString code;
BunString message;
Expand Down
18 changes: 15 additions & 3 deletions src/runtime/dns_jsc/cares_jsc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,18 @@ fn any_reply_to_js(
}

// ── Error ──────────────────────────────────────────────────────────────────

/// Node only gives a DNS error a numeric `errno` when libuv reported one
/// (`getaddrinfo`/`getnameinfo`). A c-ares resolver failure (`query*`,
/// `getHostByAddr`) only has a string `code`, so its `errno` is undefined.
Comment thread
robobun marked this conversation as resolved.
fn errno_for_syscall(this: c_ares::Error, syscall: &[u8]) -> c_int {
if strings::has_prefix_comptime(syscall, b"query") || strings::eql(syscall, b"getHostByAddr") {
SystemError::NO_ERRNO
} else {
this as c_int
}
}

pub(crate) struct ErrorDeferred {
pub errno: c_ares::Error,
pub syscall: &'static [u8],
Expand Down Expand Up @@ -679,7 +691,7 @@ impl ErrorDeferred {
))
};
let system_error = SystemError {
errno: self.errno as i32,
errno: errno_for_syscall(self.errno, self.syscall),
code: bstr::String::static_(code).into(),
message: message.into(),
syscall: bstr::String::clone_utf8(self.syscall).into(),
Expand Down Expand Up @@ -765,7 +777,7 @@ pub(crate) fn error_to_js_with_syscall(
) -> JsResult<JSValue> {
let code = this.code();
let instance = SystemError {
errno: this as i32,
errno: errno_for_syscall(this, syscall),
code: bstr::String::static_(&code[4..]).into(),
syscall: bstr::String::static_(syscall).into(),
message: bstr::String::create_format(format_args!(
Expand Down Expand Up @@ -797,7 +809,7 @@ pub(crate) fn system_error_with_syscall_and_hostname(
) -> SystemError {
let code = this.code();
SystemError {
errno: this as i32,
errno: errno_for_syscall(this, syscall),
code: bstr::String::static_(&code[4..]).into(),
message: bstr::String::create_format(format_args!(
"{} {} {}",
Expand Down
97 changes: 97 additions & 0 deletions test/js/node/dns/node-dns.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,101 @@ test.skipIf(isWindows)("dns.resolveSrv accepts compressed target in RDATA", asyn
}
});

// Node only sets a numeric `errno` on DNS errors when libuv reported a
// numeric error (getaddrinfo/getnameinfo). c-ares resolver failures carry a
// string code and leave `errno` undefined. https://github.com/oven-sh/bun/issues/37320
test.skipIf(isWindows)("resolver query errors leave errno undefined, lookup errors keep a number", async () => {
const socket = dgram.createSocket("udp4");
try {
socket.on("message", (query, rinfo) => {
// Reply NXDOMAIN to every query.
const res = Buffer.from(query);
res[2] = 0x81; // QR=1, RD=1
res[3] = 0x83; // RA=1, RCODE=3 (NXDOMAIN)
res.fill(0, 6, 12); // ANCOUNT/NSCOUNT/ARCOUNT = 0
socket.send(res, rinfo.port, rinfo.address);
});
socket.bind(0, "127.0.0.1");
await once(socket, "listening");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const { port } = socket.address();

const servers = ["127.0.0.1:" + port];
const resolver = new dns.Resolver({ timeout: 1000, tries: 1 });
resolver.setServers(servers);
const promisesResolver = new dns.promises.Resolver({ timeout: 1000, tries: 1 });
promisesResolver.setServers(servers);

const unexpectedSuccess = new Error("expected the query to fail");
const callbackError = query =>
new Promise((resolve, reject) => query(err => (err ? resolve(err) : reject(unexpectedSuccess))));
const promiseError = promise =>
promise.then(
() => {
throw unexpectedSuccess;
},
err => err,
);

const errors = {
resolve4: await callbackError(cb => resolver.resolve4("invalid.invalid", cb)),
resolveAny: await callbackError(cb => resolver.resolveAny("invalid.invalid", cb)),
reverse: await callbackError(cb => resolver.reverse("192.0.2.1", cb)),
promisesResolve4: await promiseError(promisesResolver.resolve4("invalid.invalid")),
promisesResolveAny: await promiseError(promisesResolver.resolveAny("invalid.invalid")),
promisesReverse: await promiseError(promisesResolver.reverse("192.0.2.1")),
// dns.lookup() rejects a name with a NUL byte before it reaches a resolver
// backend, so this getaddrinfo failure does not need the network either.
lookup: await promiseError(dns.promises.lookup("invalid.invalid\0")),
};

// Node keeps `errno` as an own property of the error and only leaves its
// value undefined, so check the property is still there.
const shapes = Object.fromEntries(
Object.entries(errors).map(([name, err]) => [
name,
{
code: err.code,
syscall: err.syscall,
hostname: err.hostname,
errno: err.errno,
hasOwnErrno: Object.hasOwn(err, "errno"),
},
]),
);
const query = syscall => ({
code: "ENOTFOUND",
syscall,
hostname: "invalid.invalid",
errno: undefined,
hasOwnErrno: true,
});
const reverse = {
code: "ENOTFOUND",
syscall: "getHostByAddr",
hostname: "192.0.2.1",
errno: undefined,
hasOwnErrno: true,
};
expect(shapes).toEqual({
resolve4: query("queryA"),
resolveAny: query("queryAny"),
reverse,
promisesResolve4: query("queryA"),
promisesResolveAny: query("queryAny"),
promisesReverse: reverse,
lookup: {
code: "ENOTFOUND",
syscall: "getaddrinfo",
hostname: "invalid.invalid\0",
errno: expect.any(Number),
hasOwnErrno: true,
},
});
} finally {
socket.close();
}
});

test("dns.resolveTxt (txt.socketify.dev)", () => {
const { promise, resolve, reject } = Promise.withResolvers();
dns.resolveTxt("txt.socketify.dev", (err, results) => {
Expand Down Expand Up @@ -404,6 +499,8 @@ test("dns.lookup bad (qedjp3f4q4jgjh4d6vaf3fd2hbfhg6upt2bscrfe.com)", () => {
expect(err).not.toBeNull();
expect(err.syscall).toEqual("getaddrinfo");
expect(err.code).toEqual("ENOTFOUND");
// Unlike resolver query errors, lookup errors keep a numeric errno in Node.
expect(err.errno).toBeNumber();
expect(address).toBeUndefined();
expect(family).toBeUndefined();
resolve();
Expand Down
Loading