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:
- 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.
- 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
Summary
dns.resolveCaa()anddns.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
dataencoding (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:src/rust/api/dns.rsonly understands the old encoding, sonode:dnsbreaks wherever the new format is being served.Reproduction
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 fieldsBecause 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')insrc/node/internal/internal_dns_client.ts(which requestsapplication/dns-json) βnormalizeCaaβdnsUtil.parseCaaRecord(data)βparse_caa_recordinsrc/rust/api/dns.rs.parse_caa_recordsplits on whitespace and unconditionally treats the first two tokens as the\#marker and the rdata length: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:datavalue0 issue "pki.goog"(google.com)data.len() == 1βInvalidDnsResponse("CAA record data too short: expected critical and prefix length fields")0 issue "digicert.com; cansignhttpexchanges=yes"(cloudflare.com)data[0].parse::<u8>()on"digicert.com;βParseIntErrorβRangeError: invalid digit found in stringNeither is correct, but at least neither silently corrupts:
data[0]always begins with"in presentation format, soparse::<u8>()can never succeed by accident.resolveNaptris broken the same wayparse_naptr_recordhas the same structure. Presentation format100 10 "s" "SIP+D2U" "" _sip._udp.example.com.yields 6 tokens, sodata = parts[1..]has 5, trippingdata.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 bothparse_caa_recordandparse_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:cloudflare.compublishes0 issue "digicert.com; cansignhttpexchanges=yes", whose value contains;(3b 20in the hex form). The same applies to NAPTR's quotedregexpfield.replacementarrives with a trailing dot in presentation format (_sip._udp.example.com.).parse_replacementalready 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::testsare 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 forresolveTxtquoting), switchingsendDnsRequestoffapplication/dns-jsonto the DoH wireformat would remove this whole class of breakage. That is a much bigger change β it touches everynormalize*helper ininternal_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/CDNSKEYswitch to numeric DNSSEC algorithm identifiers, andHINFOcharacter-strings are now individually quoted.node:dnsdoesn'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-sdkWrangler E2E suite, which callsresolveCaa("google.com")inpackages/wrangler/e2e/unenv-preset/worker/index.ts. It has been failing intermittently onmainsince 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.comqueried from LHR still returns\# 15 00 05 69 73 73 75 65 70 6b 69 2e 67 6f 6f 67forgoogle.com, whiledns.googlealready returns0 issue "pki.goog"β the 3-token shape that produces the reported error.Related