Skip to content

πŸ› Bug Report β€” node:dns resolveCaa/resolveNaptr throw after the 1.1.1.1 DoH JSON presentation-format rolloutΒ #6912

Description

@petebacondarwin

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 (changelog 2026-07-28), replacing RFC 3597 generic hex (\# <length> <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 generic hex encoding (\# <length> <hex>). 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

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:

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::<u8>()?;
let prefix_length = data[1].parse::<usize>()?;

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::<u8>() 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::<u8>() 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions