cloudflare / cloudflare/workerd
🐛 Bug Report — node:dns resolveCaa/resolveNaptr throw after the 1.1.1.1 DoH JSON presentation-format rollout
- 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
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