Skip to content
Merged
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
29 changes: 27 additions & 2 deletions crates/perry-runtime/src/dns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -825,14 +825,39 @@ fn dns_error_code(err: DnsError) -> &'static str {
}
}

/// Build a c-ares-style DNS error carrying Node's `.code`, `.syscall`, and
/// `.hostname` own properties. Node's `dns.resolve*`/`dns.reverse` rejections
/// always set all three (with `errno` left `undefined`); registering them on
/// the message StringHeader mirrors the `.code` path so caught errors expose
/// the full shape, not just `.code`.
fn dns_query_error_value(
message: &str,
code: &'static str,
syscall: &'static str,
hostname: &str,
) -> f64 {
let msg = crate::string::js_string_from_bytes(message.as_ptr(), message.len() as u32);
crate::node_submodules::register_error_code_pub(msg, code);
crate::node_submodules::register_error_syscall(msg, syscall);
crate::node_submodules::register_error_hostname(msg, hostname.to_string());
let err = crate::error::js_error_new_with_message(msg);
boxed_pointer(err as *const u8)
}

fn resolve_error_value(kind: RecordKind, host: &str, err: DnsError) -> f64 {
let code = dns_error_code(err);
plain_error_value(&format!("{} {code} {host}", resolve_syscall(kind)), code)
let syscall = resolve_syscall(kind);
dns_query_error_value(&format!("{syscall} {code} {host}"), code, syscall, host)
}

fn reverse_error_value(host: &str, err: DnsError) -> f64 {
let code = dns_error_code(err);
plain_error_value(&format!("getHostByAddr {code} {host}"), code)
dns_query_error_value(
&format!("getHostByAddr {code} {host}"),
code,
"getHostByAddr",
host,
)
}

/// Deterministic-mode (`PERRY_DETERMINISTIC_NET=1`) loopback answers — the
Expand Down
25 changes: 25 additions & 0 deletions crates/perry-runtime/src/node_submodules/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,31 @@ pub fn error_path_for_message(message_ptr: *const StringHeader) -> Option<String
ERROR_MESSAGE_PATHS.with(|m| m.borrow().get(&(message_ptr as usize)).cloned())
}

thread_local! {
pub(crate) static ERROR_MESSAGE_HOSTNAMES: RefCell<HashMap<usize, String>> =
RefCell::new(HashMap::new());
}

/// Attach a Node-style `hostname` string to an Error keyed by its message
/// StringHeader. Node's c-ares `dns.resolve*`/`dns.reverse` failures carry the
/// queried hostname (or address) on `err.hostname`. Read back from the
/// `.hostname` getter in `field_get_set` (mirrors `.path`).
pub fn register_error_hostname(message_ptr: *const StringHeader, hostname: String) {
if message_ptr.is_null() {
return;
}
ERROR_MESSAGE_HOSTNAMES.with(|m| {
m.borrow_mut().insert(message_ptr as usize, hostname);
});
}

pub fn error_hostname_for_message(message_ptr: *const StringHeader) -> Option<String> {
if message_ptr.is_null() {
return None;
}
ERROR_MESSAGE_HOSTNAMES.with(|m| m.borrow().get(&(message_ptr as usize)).cloned())
}

/// Attach a Node-style `dest` string to an Error keyed by its message
/// StringHeader. Node sets `dest` on two-path fs errors (rename/copyFile/
/// link/symlink) alongside `path`. Read back from the `.dest` getter.
Expand Down
15 changes: 15 additions & 0 deletions crates/perry-runtime/src/object/field_get_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4205,6 +4205,21 @@ pub extern "C" fn js_object_get_field_by_name(
}
return JSValue::undefined();
}
b"hostname" => {
// Node attaches `hostname` to c-ares dns errors
// (`dns.resolve*`/`dns.reverse`). Mirrors `.path`.
let msg = crate::error::js_error_get_message(err_ptr);
if let Some(hostname) =
crate::node_submodules::error_hostname_for_message(msg)
{
let s = crate::string::js_string_from_bytes(
hostname.as_ptr(),
hostname.len() as u32,
);
return JSValue::from_bits(crate::js_nanbox_string(s as i64).to_bits());
}
return JSValue::undefined();
}
b"dest" => {
// Node attaches `dest` to two-path fs errors
// (rename/copyFile/link/symlink). Mirrors `.path`.
Expand Down
16 changes: 14 additions & 2 deletions test-parity/node-suite/dns/imports/default-export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,17 @@ console.log(
);

const resolver = new dnsPromisesDefault.Resolver();
const resolver4 = await resolver.resolve4("localhost");
console.log("promises default resolve4:", Array.isArray(resolver4), JSON.stringify(resolver4));
// resolve4 queries the configured nameserver directly (it does not read
// /etc/hosts), so "localhost" yields an A record on some resolvers and
// ESERVFAIL/ENOTFOUND on others — node itself rejects (and aborts on the
// unhandled rejection) when the resolver has no answer for it. Print
// array-or-error-code so node and Perry agree regardless of the machine's
// resolver, the same record-or-error approach the dns/resolve suite uses.
let resolver4Summary: string;
try {
const resolver4 = await resolver.resolve4("localhost");
resolver4Summary = Array.isArray(resolver4) ? "array" : typeof resolver4;
} catch (e: any) {
resolver4Summary = "err:" + e.code;
}
console.log("promises default resolve4:", resolver4Summary);