cloudflare / cloudflare/workerd

🐛 Bug Report — node:dns resolveCaa/resolveNaptr throw after the 1.1.1.1 DoH JSON presentation-format rollout

Open
#6,912 0 comments 0 reactions 0 assignees View on GitHub
bug nodejs compat
Dominant language
C++
Stars
8.7k
Forks
739
Avg merge
2d 20h
Merged PRs (30d)
174

Description

## Summary

`dns.resolveCaa()` and `dns.resolveNaptr()` throw for valid records, intermittently, depending on which 1.1.1.1 instance serves the request.

Cloudflare's own 1.1.1.1 DoH JSON API is [rolling out a new `data` encoding](https://developers.cloudflare.com/changelog/post/2026-07-28-improved-record-display-format/) (changelog 2026-07-28), replacing RFC 3597 generic hex (`\# `) with standard presentation format for CAA, NAPTR, TLSA, SVCB, HTTPS, SSHFP, RP, IPSECKEY and OPENPGPKEY:

> Several record types previously returned their `data` field in [RFC 3597](https://datatracker.ietf.org/doc/html/rfc3597) generic hex encoding (`\# `). These now use standard presentation format:
> ```
> CAA: 0 issue "letsencrypt.org"
> NAPTR: 100 10 "s" "SIP+D2U" "" _sip._udp.example.com.
> ```
> … **During the roll out responses may use either the old or new format.**
>
> These are breaking changes. The DoH JSON format has no formal RFC and its schema is not guaranteed to be stable. If you need a stable format, use the DoH wireformat instead.

`src/rust/api/dns.rs` only understands the old encoding, so `node:dns` breaks wherever the new format is being served.

## Reproduction

```js
import dns from "node:dns/promises";

export default {
async fetch() {
return Response.json(await dns.resolveCaa("google.com"));
},
};
```

Expected (Node.js): `[{ "critical": 0, "issue": "pki.goog" }]`

Actual, where the new format is served: `Error: CAA record data too short: expected critical and prefix length fields`

Because the rollout is partial, this reproduces on some colos and not others. From a colo still on the old format the same request succeeds.

## Root cause

`resolveCaa` → `sendDnsRequest(name, 'CAA')` in `src/node/internal/internal_dns_client.ts` (which requests `application/dns-json`) → `normalizeCaa` → `dnsUtil.parseCaaRecord(data)` → `parse_caa_record` in `src/rust/api/dns.rs`.

`parse_caa_record` splits on whitespace and unconditionally treats the first two tokens as the `\#` marker and the rdata length:

```rust
let parts: Vec<_> = record.split_ascii_whitespace().collect();
if parts.len() < 3 { /* "CAA record too short: expected at least 3 fields" */ }
let data = parts[2..].to_vec();
if data.len() < 2 { /* "CAA record data too short: expected critical and prefix length fields" */ }
let critical = data[0].parse::()?;
let prefix_length = data[1].parse::()?;
```

Presentation format has no `\#` marker and no length prefix, so the token offsets are wrong. Which error you get depends on the record, and there are two distinct shapes:

| `data` value | tokens | outcome |
|---|---|---|
| `0 issue "pki.goog"` (google.com) | 3 | `data.len() == 1` → `InvalidDnsResponse("CAA record data too short: expected critical and prefix length fields")` |
| `0 issue "digicert.com; cansignhttpexchanges=yes"` (cloudflare.com) | 4 | guards pass, then `data[0].parse::()` on `"digicert.com;` → `ParseIntError` → `RangeError: invalid digit found in string` |

Neither is correct, but at least neither silently corrupts: `data[0]` always begins with `"` in presentation format, so `parse::()` can never succeed by accident.

## `resolveNaptr` is broken the same way

`parse_naptr_record` has the same structure. Presentation format `100 10 "s" "SIP+D2U" "" _sip._udp.example.com.` yields 6 tokens, so `data = parts[1..]` has 5, tripping `data.len() < 6` → `NAPTR record data too short: expected at least 6 fields`.

## Proposed fix

Branch on whether the record starts with `\#` and add presentation-format paths to both `parse_caa_record` and `parse_naptr_record`, keeping the existing hex paths for the duration of the rollout (and for any resolver that still emits the old encoding). Two traps worth flagging for whoever picks this up:

1. **CAA values can contain whitespace**, so the value must be taken as everything after the tag with surrounding quotes stripped — not whitespace-split. `cloudflare.com` publishes `0 issue "digicert.com; cansignhttpexchanges=yes"`, whose value contains `; ` (`3b 20` in the hex form). The same applies to NAPTR's quoted `regexp` field.
2. **NAPTR `replacement` arrives with a trailing dot** in presentation format (`_sip._udp.example.com.`). `parse_replacement` already strips it for the hex path to match Node.js; the new path needs to as well.

The existing malformed-input tests in `dns.rs::tests` are a good place to add coverage for both encodings.

### Durable fix

The changelog is explicit that the JSON schema is not stable and recommends the wireformat. Since this is the second time the JSON encoding has moved under `node:dns` (cf. #3327, #3330 for `resolveTxt` quoting), switching `sendDnsRequest` off `application/dns-json` to the DoH wireformat would remove this whole class of breakage. That is a much bigger change — it touches every `normalize*` helper in `internal_dns_client.ts` — so it seems like a follow-up rather than the immediate fix.

### Also in that changelog, not affected today

`RRSIG`/`DS`/`CDS`/`DNSKEY`/`CDNSKEY` switch to numeric DNSSEC algorithm identifiers, and `HINFO` character-strings are now individually quoted. `node:dns` doesn't implement those record types, so there's nothing to do now, but they're worth knowing about before any of them get added.

## Impact / evidence

This was found via the `workers-sdk` Wrangler E2E suite, which calls `resolveCaa("google.com")` in `packages/wrangler/e2e/unenv-preset/worker/index.ts`. It has been failing intermittently on `main` since 2026-07-30, two days after the changelog, across 15 of the recent runs I sampled and on both Linux and macOS runners. Within a single job it fails deterministically for the full retry window (same colo, cached answer), which is what distinguishes it from ordinary DNS flakiness.

Cross-checking the encodings, as of today: `cloudflare-dns.com` queried from LHR still returns `\# 15 00 05 69 73 73 75 65 70 6b 69 2e 67 6f 6f 67` for `google.com`, while `dns.google` already returns `0 issue "pki.goog"` — the 3-token shape that produces the reported error.

## Related

- #6311 added the bounds checks that now surface this as an error rather than an index-out-of-bounds panic
- #6748 (open) — saturating arithmetic in the same length checks
- #6827 (open) — harden tls/dns/net/WebCrypto checks

Contributor guide

Open the contributing guide

Research direction

Start with parse_caa_record and parse_naptr_record in src/rust/api/dns.rs, then trace their callers through src/node/internal/internal_dns_client.ts. Run the existing malformed-input tests in dns.rs::tests and inspect the workers-sdk reproduction in packages/wrangler/e2e/unenv-preset/worker/index.ts. Done means both old RFC 3597 and new presentation-format CAA/NAPTR responses parse correctly, including quoted whitespace and trailing-dot replacement cases.

Written by the indexing model from the issue text.

Assessment

Tech stack
node.js, rust, typescript
Domain
backend-api-design, networking
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.