DoH Wire Format (RFC 8484)

Send application-scoped ResolveDB DNS queries over HTTPS using RFC 8484 wire format.

Endpoint

https://doh.resolvedb.io/dns-query

ResolveDB is authoritative-only. Use this endpoint for ResolveDB qnames; do not configure it as a browser or operating-system resolver for unrelated domains.

GET

Build a DNS message, encode it as unpadded Base64url, and send it in the dns query parameter:

GET /dns-query?dns={base64url-dns-message}
Accept: application/dns-message

The encoded parameter is limited to 8 KiB and the decoded DNS message is limited to 4 KiB. Generate the bytes with a DNS library rather than hand-encoding names.

POST

Send a DNS wire message as the request body:

POST /dns-query
Content-Type: application/dns-message
Accept: application/dns-message

{binary DNS message}

The request body is limited to 4 KiB.

Python Example

import dns.message
import dns.query

query = dns.message.make_query(
    "get.newyork.weather.public.v1.resolvedb.net",
    "TXT",
)
response = dns.query.https(
    query,
    "https://doh.resolvedb.io/dns-query",
)

for rrset in response.answer:
    for record in rrset:
        print(record.to_text())

Go Example

package main

import (
    "encoding/base64"
    "fmt"
    "io"
    "net/http"
    "net/url"
    "strings"

    "github.com/miekg/dns"
)

func main() {
    message := new(dns.Msg)
    message.SetQuestion("get.newyork.weather.public.v1.resolvedb.net.", dns.TypeTXT)

    wire, err := message.Pack()
    if err != nil {
        panic(err)
    }

    encoded := base64.RawURLEncoding.EncodeToString(wire)
    endpoint := "https://doh.resolvedb.io/dns-query?dns=" + url.QueryEscape(encoded)
    request, err := http.NewRequest(http.MethodGet, endpoint, nil)
    if err != nil {
        panic(err)
    }
    request.Header.Set("Accept", "application/dns-message")

    response, err := http.DefaultClient.Do(request)
    if err != nil {
        panic(err)
    }
    defer response.Body.Close()

    body, err := io.ReadAll(response.Body)
    if err != nil {
        panic(err)
    }
    answer := new(dns.Msg)
    if err := answer.Unpack(body); err != nil {
        panic(err)
    }

    for _, rr := range answer.Answer {
        if txt, ok := rr.(*dns.TXT); ok {
            fmt.Println(strings.Join(txt.Txt, ""))
        }
    }
}

Responses

Successful HTTP handling returns application/dns-message; inspect the DNS RCODE for query-level failures.

RCODEMeaning
NOERRORAnswer or NODATA
FORMERRMalformed UQRP input
SERVFAILService or storage failure
REFUSEDAuthorization, namespace, or reserved-resource denial

Wire responses with answers use Cache-Control: max-age=<minimum DNS TTL>. Errors, empty answers, and private TTL-0 answers use Cache-Control: no-store. TXT records may contain multiple 255-byte character strings; concatenate them in order before parsing the UQRP envelope.

HTTP Errors

StatusMeaning
200DNS response returned; inspect its RCODE
400Missing, malformed, or oversized DNS message
406Accept does not allow application/dns-message
415POST body has the wrong media type

Shared Answering Contract (Implemented)

The shared answering contract requires wire DoH and native DNS to preserve the same product value, RCODE, and effective DNS TTL. Requested DNSSEC material uses the same live zone/key lifecycle, with supporting DNSKEY access; required signing failure becomes a DNS failure. Query statistics include DoH.

Shared schema/zone and units/moon/gated-BTC answering are implemented in the repository, including those value/RCODE/TTL/signing guarantees. Computed ordinary answers retain flat fields after v=rdb1;s=ok;t=data; BTC remains mock-only and gated off. Hosted private/public-read/demo records now share these guarantees: private hits and negatives carry TTL 0 through SOA/proofs, public positive TTLs are capped at record expiry, and corrupt values return SERVFAIL. All-family evaluation statistics include DoH. Weather, forecast, sun, and GeoIP share output/signing/cache policy too: forecast retains daily provider values and TTL at most 1800; daily results are capped at UTC midnight. Dataset manifest/keys/identity answering and analytics are also shared. Statistics identify the evaluated family: hosted units is records, datasets are datasets. Only verified private authorization supplies tenant attribution, including on misses and expiry. Public/schema/dataset reads remain unattributed. Complete repository conformance does not imply production rollout or package publication.

Product answers are TXT-only; ordinary zone and DNSSEC support records are exempt. Cacheable TTLs are bounded by known expiry or validity transitions, and authenticated positive and negative answers are uncacheable. Encoding the entire DNS message in the GET dns= parameter remains unpadded Base64url; retiring Base64 inside query labels does not change RFC 8484 framing.

Next Steps