Skip to content

dns: match Node's getaddrinfo error shape (errno/hostname/message/name)#30987

Open
alii wants to merge 1 commit into
mainfrom
ali/dns-lookup-error-node-parity
Open

dns: match Node's getaddrinfo error shape (errno/hostname/message/name)#30987
alii wants to merge 1 commit into
mainfrom
ali/dns-lookup-error-node-parity

Conversation

@alii

@alii alii commented May 18, 2026

Copy link
Copy Markdown
Member

dns.lookup (getaddrinfo) failures produced an error object that diverged from Node on four fields:

  • errno was the positive c-ares enum value (4) instead of libuv's negative UV_EAI_NONAME (-3008)
  • hostname was undefined instead of the queried name
  • message was "getaddrinfo ENOTFOUND" (no hostname suffix)
  • name was "DNSException" instead of "Error"

Thread the queried name onto the DNSLookup node so every getaddrinfo failure path can build the Node-shaped error (this also fixes message, same root cause). Map the c-ares error to libuv's negative EAI_* errno for the getaddrinfo syscall, and use name: "Error" like Node (also corrects resolve*/reverse, which Node likewise reports as Error).

Pre-existing (Zig 1.3.14 has the same bug), so the test lives in the dns module. It's node:test/node:assert and runs under both bun test and node --experimental-strip-types --test: green under Node, fails under released Bun, green with this fix. dns.lookup output is now byte-identical to Node; node-dns.test.js (66) and vendored test-dns-lookup.js/test-dns.js still pass.

@robobun

robobun commented May 18, 2026

Copy link
Copy Markdown
Collaborator
Updated 5:35 AM PT - May 30th, 2026

@robobun, your commit 6f481629c91a65ffb2d0723e99f549396dfcf351 passed in Build #59223! 🎉


🧪   To try this PR locally:

bunx bun-pr 30987

That installs a local version of the PR into your bun-30987 executable, so you can run:

bun-30987 --bun

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e2af04a7-6636-447e-93b6-52e127e8c95b

📥 Commits

Reviewing files that changed from the base of the PR and between 0175fcb and 93acccc.

📒 Files selected for processing (1)
  • src/runtime/dns_jsc/cares_jsc.rs

Walkthrough

This PR extends DNS error handling to include the queried hostname, maps c-ares getaddrinfo errors to libuv-style negative EAI_* errno values, and converts DNS error instances’ name to "Error" so JS APIs emit Node-shaped getaddrinfo ENOTFOUND errors including errno and hostname.

Changes

DNS error shape alignment

Layer / File(s) Summary
DNSLookup hostname field and init signature
src/runtime/dns_jsc/dns.rs
Adds name: Box<[u8]> field to the DNSLookup struct and updates the DNSLookup::init initializer to accept a hostname parameter, storing it for later use in error construction.
Hostname threading through init call sites
src/runtime/dns_jsc/dns.rs
Updates macOS libinfo, non-Windows libc, Windows uv_getaddrinfo, c-ares normalized-name, and GetAddrInfoRequest initialization paths to pass the queried hostname when constructing DNSLookup instances from inflight-cache hits.
Error object construction with hostname and errno mapping
src/runtime/dns_jsc/cares_jsc.rs, src/runtime/dns_jsc/dns.rs
Adds getaddrinfo_uv_errno helper to map selected c_ares::Error variants to libuv negative EAI_* constants; updates ErrorDeferred::reject to apply the mapping for getaddrinfo/getnameinfo syscalls while preserving raw c-ares errno for other syscalls; changes deferred and synchronous error instance name from "DNSException" to "Error"; and includes the stored hostname when constructing deferred errors.
Node.js API wiring and http client changes
src/js/node/dns.ts, src/js/node/_http_client.ts
Passes hostname through to empty-result handling and promise helpers so dns.lookup and promises.lookup generate Node-shaped getaddrinfo ENOTFOUND <host> errors including errno and hostname; updates internal http client DNS no-candidates path to emit the same shaped error.
Error validation test suite
test/js/node/dns/lookup-error.test.ts, test/js/bun/dns/resolve-dns.test.ts
Adds a Node-oriented test verifying Node-shaped getaddrinfo ENOTFOUND errors and updates Bun DNS tests to expect name: "Error" and Node-like fields (code, syscall, hostname, errno, and message) on dns.lookup failures.
Misc native binding
src/jsc/bindings/wtf-bindings.cpp
Replaces assert(tio != NULL) with project ASSERT(tio != NULL) in uv__tty_make_raw.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main objective of the changeset: aligning Bun's dns.lookup/getaddrinfo error objects with Node.js by fixing four key fields (errno, hostname, message, name).
Description check ✅ Passed The description provides clear context for the changes, explaining the four diverging fields, the solution approach, and verification details; it matches the template structure with 'What' and 'How verified' sections adequately covered.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Native fetch incompatibilities with NodeJS error format and codes #20486 - Case 1 explicitly expects errno: -3008 (negative libuv code), hostname field, and getaddrinfo ENOTFOUND <hostname> message format on DNS errors — all four fields this PR fixes

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #20486

🤖 Generated with Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/node/dns/lookup-error.test.ts`:
- Around line 47-50: The test "dns.lookup({ all: true }) failure has a
Node-shaped error" calls lookupError(HOST, { all: true }) and asserts shape via
assertNodeShape(err, HOST) but omits checking err.message; add an assertion that
the error message equals the Node-like string (e.g., `getaddrinfo ENOTFOUND
${HOST}`) after obtaining err to mirror the other tests—use
assert.strictEqual(err.message, `getaddrinfo ENOTFOUND ${HOST}`) alongside the
existing assertNodeShape call.
- Around line 41-58: These three independent tests ("dns.lookup failure has a
Node-shaped error", "dns.lookup({ all: true }) failure has a Node-shaped error",
and "dnsPromises.lookup failure has a Node-shaped error") can be run in
parallel; change their declarations from test(...) to test.concurrent(...) so
Jest runs them concurrently, leaving the test bodies (calls to lookupError,
assertNodeShape, assert.rejects, and assertions) unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a99a3724-08e6-4a50-ba49-d8822375eeee

📥 Commits

Reviewing files that changed from the base of the PR and between 4c8a21b and efcc459.

📒 Files selected for processing (3)
  • src/runtime/dns_jsc/cares_jsc.rs
  • src/runtime/dns_jsc/dns.rs
  • test/js/node/dns/lookup-error.test.ts

Comment thread test/js/node/dns/lookup-error.test.ts
Comment thread test/js/node/dns/lookup-error.test.ts

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional findings (outside current diff — PR may have been updated during review):

  • 🟡 src/runtime/dns_jsc/dns.rs:2084-2099 — nit: the .clone() here (and at lines 2053 and 2094) heap-allocates a fresh Box<[u8]> only to reborrow it as &[u8]error_to_deferred already copies the bytes via bstr::String::clone_utf8, so the intermediate alloc is redundant. The six existing call sites in this file (e.g. dns.rs:983/997, 1773/1784, 1914/1925) pass Some(&(*this).name) directly alongside &mut (*this).promise, so you can drop the clone for consistency.

    Extended reasoning...

    What

    At dns.rs:2053, 2085, and 2094 the new error paths do:

    let host = (*this).name.clone();
    error_to_deferred(err, b"getaddrinfo", Some(&host[..]), &mut (*this).promise)

    (*this).name is a Box<[u8]>, so .clone() heap-allocates a new boxed slice and copies the hostname bytes into it. The very next line reborrows that fresh allocation as &[u8] and hands it to error_to_deferred, which (cares_jsc.rs:798–806) does hostname.map(bstr::String::clone_utf8) — copying the bytes a second time into the owned bstr::String it actually keeps. The intermediate Box<[u8]> is then dropped at scope exit, having served only as a staging buffer.

    Why it is unnecessary

    error_to_deferred takes Option<&[u8]>; it never needs an owned buffer from the caller. Borrowing (*this).name as &[u8] while simultaneously taking &mut (*this).promise is fine: they are disjoint fields accessed through a raw-pointer place expression, which the borrow checker accepts. This is not hypothetical — the same file already does exactly that at six call sites:

    • dns.rs:983 / dns.rs:997Some((*this).name.as_ref()) with &mut (*this).promise
    • dns.rs:1773 / dns.rs:1784Some(&(*this).name) with &mut (*this).promise
    • dns.rs:1914 / dns.rs:1925Some(&(*this).name) with &mut (*this).promise

    So the clone is not required for borrow-check reasons, and it is inconsistent with the established pattern everywhere else in the file.

    Step-by-step

    1. dns.lookup("foo.invalid") fails; process_get_addr_info_native is called with an error status.
    2. (*this).name holds b"foo.invalid" (11 bytes, one heap alloc).
    3. let host = (*this).name.clone(); → second heap alloc, 11 bytes copied.
    4. error_to_deferred(.., Some(&host[..]), ..)bstr::String::clone_utf8 makes a third copy into the bstr::String stored on ErrorDeferred.
    5. host drops; the second alloc is freed having contributed nothing the original (*this).name could not have provided directly.

    Impact

    Negligible in practice — error path only, hostnames are ≤253 bytes, one extra small alloc/free per failed lookup. There is no correctness issue. This is purely a consistency/cleanliness nit.

    Fix

    Drop the clone and pass the field directly, matching the other call sites:

    error_to_deferred(err, b"getaddrinfo", Some(&(*this).name), &mut (*this).promise)
        .reject_later((*this).global_this());

    Apply the same change at all three locations (2053, 2085, 2094).

Comment thread src/runtime/dns_jsc/cares_jsc.rs Outdated
Comment thread src/runtime/dns_jsc/cares_jsc.rs Outdated
Comment thread src/runtime/dns_jsc/dns.rs
@alii

alii commented May 18, 2026

Copy link
Copy Markdown
Member Author

@robobun fix review comments

Comment thread src/runtime/dns_jsc/cares_jsc.rs
Comment thread src/js/node/_http_client.ts Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — fe0b987 addresses both remaining nits (EBADNAME mapping and fail() errno/hostname). No further issues from me; deferring to a human given the cross-platform Rust DNS scope and the build failures CI flagged on b8e6dcb.

Extended reasoning...

Overview

This PR aligns dns.lookup (getaddrinfo) error objects with Node's shape across four fields (errno, hostname, message, name). It threads the queried hostname onto DNSLookup (new name: Box<[u8]> field + updated init signature) across all four backends (macOS libinfo, POSIX libc, Windows uv, c-ares), adds a getaddrinfo_uv_errno() c-ares→libuv errno mapping table, flips three b"DNSException" literals to b"Error" in cares_jsc.rs, reshapes the JS-side empty-result paths in dns.ts (throwIfEmpty) and _http_client.ts (fail()), updates resolve-dns.test.ts expectations, and adds a dual-runtime lookup-error.test.ts. There's also an unrelated one-line drive-by in wtf-bindings.cpp (assertASSERT).

Prior feedback addressed

Across three review rounds I flagged: (1) leftover DNSException literals in the sync-throw helpers, (2) resolve-dns.test.ts would break, (3) throwIfEmpty() still emitted the old shape, (4) getaddrinfo_uv_errno() missing the EBADNAME arm, and (5) _http_client.ts fail() not setting errno/hostname. All five are now fixed in the current diff (commits f7c4ee8 and fe0b987). No new issues found this round.

Security risks

None identified. The change only reshapes error metadata on already-failing DNS lookups; no new untrusted-input parsing, no auth/crypto/permissions surface.

Level of scrutiny

Moderate. DNS resolution is a hot, cross-platform runtime path; the DNSLookup struct gains a per-lookup heap allocation and its init signature changes across four platform-gated call sites that I can't all exercise locally. The err.name change from "DNSException""Error" is a user-visible behavior change (correct per Node, but could affect Bun-specific code that pattern-matches on the old name). Buildkite flagged failures on b8e6dcb (per my earlier read, build-infra on aarch64-android / x64-asan / freebsd rather than test failures), and CI status on the latest fe0b987 isn't yet reflected in the robobun comment.

Other factors

Good test coverage (new dual-runtime test plus updated Bun-API test); the author has been responsive to every round of feedback. The drive-by wtf-bindings.cpp change is trivially correct. Overall the change looks right to me, but the combination of cross-platform Rust runtime edits, a user-visible error-shape change, and unresolved CI status makes this one a human should sign off on rather than a bot.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/js/node/dns.ts`:
- Around line 672-682: Introduce a shared constant UV_EAI_NONAME = -3008 (near
other UV_* constants) and replace the hardcoded -3008 assignment in throwIfEmpty
(the err.errno = -3008 line inside function throwIfEmpty) and the corresponding
errno usage in the other file (the assignment at the location previously using
-3008 in _http_client.ts) to use UV_EAI_NONAME instead; ensure imports/exports
are added so throwIfEmpty and the other module reference the new constant and
keep the rest of the Error properties (code, syscall, hostname) unchanged.

In `@src/runtime/dns_jsc/cares_jsc.rs`:
- Around line 661-699: In getaddrinfo_uv_errno, change the mapping for
c_ares::Error::ECONNREFUSED (currently mapped to UV_EAI_SOCKTYPE) to use the
generic UV_EAI_FAIL constant instead so transport-level connection-refused
errors do not misreport as unsupported socket type; update the match arm for
c_ares::Error::ECONNREFUSED inside the getaddrinfo_uv_errno function
accordingly.

In `@src/runtime/dns_jsc/dns.rs`:
- Around line 224-228: The code currently clones query.name and passes the
normalized backend name into DNSLookup::init (and similar allocation sites),
causing err.hostname/message to show the rewritten name; instead, preserve the
caller-supplied hostname by threading both the original input and the normalized
lookup name separately: change DNSLookup::init (and other constructors/usages
around do_lookup and places that build GetAddrInfo) to accept two strings/byte
slices (e.g., original_hostname and lookup_name), pass Box::from(&*query.name)
as original_hostname and the normalized name as lookup_name, and make
err.hostname/message use original_hostname while lookup uses lookup_name for the
backend lookup; update related constructors and downstream fields (err.hostname,
message, any cache key logic) to retain this separation so inflight callers keep
their original hostnames.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: aa5ca73b-32ea-4f1f-8d75-6d80c196b16f

📥 Commits

Reviewing files that changed from the base of the PR and between efcc459 and 90077ce.

📒 Files selected for processing (7)
  • src/js/node/_http_client.ts
  • src/js/node/dns.ts
  • src/jsc/bindings/wtf-bindings.cpp
  • src/runtime/dns_jsc/cares_jsc.rs
  • src/runtime/dns_jsc/dns.rs
  • test/js/bun/dns/resolve-dns.test.ts
  • test/js/node/dns/lookup-error.test.ts

Comment thread src/js/node/dns.ts
Comment thread src/runtime/dns_jsc/cares_jsc.rs
Comment thread src/runtime/dns_jsc/dns.rs
Comment thread src/runtime/dns_jsc/cares_jsc.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/runtime/dns_jsc/cares_jsc.rs (1)

693-693: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

ECONNREFUSED is mapped to the wrong libuv EAI errno.

c_ares::Error::ECONNREFUSED should not map to UV_EAI_SOCKTYPE; this misreports the failure category. Use UV_EAI_FAIL as the generic fallback for this case.

Proposed fix
-        c_ares::Error::ECONNREFUSED => UV_EAI_SOCKTYPE,
+        c_ares::Error::ECONNREFUSED => UV_EAI_FAIL,
In libuv errno definitions, what does UV_EAI_SOCKTYPE mean, and is UV_EAI_FAIL the generic getaddrinfo failure code for non-specific resolver failures?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/runtime/dns_jsc/cares_jsc.rs` at line 693, The match arm mapping
c_ares::Error::ECONNREFUSED currently returns UV_EAI_SOCKTYPE, which is
incorrect; update the mapping in cares_jsc.rs so that the
c_ares::Error::ECONNREFUSED arm returns UV_EAI_FAIL instead (replace
UV_EAI_SOCKTYPE with UV_EAI_FAIL) to use the generic getaddrinfo failure code;
ensure you only change the enum/value used in the match for
c_ares::Error::ECONNREFUSED and leave other mappings untouched.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/runtime/dns_jsc/cares_jsc.rs`:
- Line 693: The match arm mapping c_ares::Error::ECONNREFUSED currently returns
UV_EAI_SOCKTYPE, which is incorrect; update the mapping in cares_jsc.rs so that
the c_ares::Error::ECONNREFUSED arm returns UV_EAI_FAIL instead (replace
UV_EAI_SOCKTYPE with UV_EAI_FAIL) to use the generic getaddrinfo failure code;
ensure you only change the enum/value used in the match for
c_ares::Error::ECONNREFUSED and leave other mappings untouched.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 56f6f8cd-a5d9-42bb-8778-db52700fa840

📥 Commits

Reviewing files that changed from the base of the PR and between 90077ce and 0175fcb.

📒 Files selected for processing (1)
  • src/runtime/dns_jsc/cares_jsc.rs

Comment thread src/runtime/dns_jsc/cares_jsc.rs Outdated
Comment thread src/runtime/dns_jsc/cares_jsc.rs
@alii

alii commented May 30, 2026

Copy link
Copy Markdown
Member Author

@robobun rebase and get this mergable

@robobun robobun force-pushed the ali/dns-lookup-error-node-parity branch from 493cdcf to 0c447de Compare May 30, 2026 09:27
Comment thread src/js/node/_http_client.ts Outdated
dns.lookup (getaddrinfo) failures produced an error object that diverged
from Node on four fields:

- errno was the positive c-ares enum value (4) instead of libuv's negative
  UV_EAI_NONAME (-3008)
- hostname was undefined instead of the queried name
- message was "getaddrinfo ENOTFOUND" (no hostname suffix)
- name was "DNSException" instead of "Error"

Thread the queried name onto the DNSLookup node so every getaddrinfo failure
path can build the Node-shaped error. Map the c-ares error to libuv's negative
EAI_* errno for getaddrinfo/getnameinfo syscalls, and let err.name inherit
from Error.prototype like Node (dropping the old DNSException put, which also
corrects resolve*/reverse). Reshape the JS-side empty-result paths in dns.ts
(throwIfEmpty) and _http_client.ts to the same Node shape.

Test lives in the dns module: it is node:test/node:assert and runs under both
bun test and node --experimental-strip-types --test.
@robobun robobun force-pushed the ali/dns-lookup-error-node-parity branch from 0c447de to 6f48162 Compare May 30, 2026 10:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants