DoH Wire Format (RFC 8484)
Standard DNS-over-HTTPS using wire format. Compatible with DNS libraries and system resolvers.
TL;DR
Send raw DNS wire format queries over HTTPS. Base64url-encode for GET, binary for POST.
# GET with base64url-encoded query
curl "https://api.resolvedb.io/dns-query?dns=AAABAAABAAAAAAAAA2dldAZ3ZWF0aGVyBnB1YmxpYwJ2MQlyZXNvbHZlZGIDbmV0AAAQAAEAADApAACOAAABAAEAACkQ..." \
-H "Accept: application/dns-message"
# POST with binary body
curl -X POST "https://api.resolvedb.io/dns-query" \
-H "Content-Type: application/dns-message" \
-H "Accept: application/dns-message" \
--data-binary @query.binWhen to Use Wire Format
- System resolver configuration - Configure OS/browser to use ResolveDB as DoH resolver
- DNS libraries - Libraries like
dnspython,trust-dns,miekg/dnssupport DoH - Compact responses - Binary format is smaller than JSON
- RFC compliance - Standard RFC 8484 implementation
For simple queries, the JSON API is easier.
Endpoint
https://api.resolvedb.io/dns-queryGET Request
Encode the DNS query in Base64url (no padding) and pass as dns parameter.
Format
GET /dns-query?dns={base64url-encoded-query}
Accept: application/dns-messageExample
# 1. Create DNS query (or use a library)
# This queries: get.newyork.weather.public.v1.resolvedb.net TXT
# 2. Base64url encode (no padding)
DNS_QUERY="AAABAAABAAAAAAAAA2dldAZ3ZWF0aGVyBnB1YmxpYwJ2MQlyZXNvbHZlZGIDbmV0AAAQAAEAADApAACOAAABAAEAACkQ"
# 3. Send request
curl "https://api.resolvedb.io/dns-query?dns=${DNS_QUERY}" \
-H "Accept: application/dns-message" \
--output response.binQuery Size Limit
Maximum 4KB decoded query size. The base64url-encoded parameter can be up to 8KB.
POST Request
Send raw DNS wire format as request body.
Format
POST /dns-query
Content-Type: application/dns-message
Accept: application/dns-message
{binary DNS message}Example
# Create query binary file using Python
import dns.message
import dns.rdatatype
query = dns.message.make_query('get.newyork.weather.public.v1.resolvedb.net', dns.rdatatype.TXT)
with open('query.bin', 'wb') as f:
f.write(query.to_wire())# Send request
curl -X POST "https://api.resolvedb.io/dns-query" \
-H "Content-Type: application/dns-message" \
-H "Accept: application/dns-message" \
--data-binary @query.bin \
--output response.binRequest Size Limit
Maximum 4KB request body.
Response Format
Both GET and POST return:
Content-Type: application/dns-message
Cache-Control: max-age={min-ttl}
{binary DNS message}For errors:
Cache-Control: no-storeBuilding DNS Queries
Python (dnspython)
import dns.message
import dns.query
import dns.rdatatype
# Build query
query = dns.message.make_query(
'get.newyork.weather.public.v1.resolvedb.net',
dns.rdatatype.TXT
)
# Send via DoH
response = dns.query.https(
query,
'https://api.resolvedb.io/dns-query'
)
# Parse response
for rrset in response.answer:
for rdata in rrset:
print(rdata.to_text())Go (miekg/dns)
package main
import (
"encoding/base64"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/miekg/dns"
)
func main() {
// Build query
msg := new(dns.Msg)
msg.SetQuestion("get.newyork.weather.public.v1.resolvedb.net.", dns.TypeTXT)
msg.RecursionDesired = true
// Pack to wire format
wire, err := msg.Pack()
if err != nil {
panic(err)
}
// GET request with base64url
encoded := base64.RawURLEncoding.EncodeToString(wire)
reqURL := fmt.Sprintf("https://api.resolvedb.io/dns-query?dns=%s", url.QueryEscape(encoded))
req, _ := http.NewRequest("GET", reqURL, nil)
req.Header.Set("Accept", "application/dns-message")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
// Read and parse response
body, _ := io.ReadAll(resp.Body)
response := new(dns.Msg)
if err := response.Unpack(body); err != nil {
panic(err)
}
// Print TXT records
for _, rr := range response.Answer {
if txt, ok := rr.(*dns.TXT); ok {
fmt.Println(strings.Join(txt.Txt, ""))
}
}
}Rust (trust-dns)
use trust_dns_client::client::{Client, SyncClient};
use trust_dns_client::op::DnsResponse;
use trust_dns_client::rr::{DNSClass, Name, RecordType};
use trust_dns_client::h2::HttpsClientConnection;
use std::str::FromStr;
fn main() {
// Configure DoH client
let conn = HttpsClientConnection::new(
"https://api.resolvedb.io/dns-query".parse().unwrap(),
);
let client = SyncClient::new(conn);
// Build and send query
let name = Name::from_str("get.newyork.weather.public.v1.resolvedb.net.").unwrap();
let response: DnsResponse = client
.query(&name, DNSClass::IN, RecordType::TXT)
.unwrap();
// Print answers
for record in response.answers() {
if let Some(txt) = record.rdata().as_txt() {
println!("{}", txt.txt_data().join(""));
}
}
}Node.js (dohjs)
const doh = require('dohjs');
const resolver = new doh.DohResolver('https://api.resolvedb.io/dns-query');
async function query() {
const response = await resolver.query('get.newyork.weather.public.v1.resolvedb.net', 'TXT');
for (const answer of response.answers) {
console.log(answer.data);
}
}
query();Manual Query Construction
DNS wire format follows RFC 1035. Here's the structure:
Query Structure
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ID | 2 bytes
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|QR| Opcode |AA|TC|RD|RA| Z | RCODE | 2 bytes
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QDCOUNT | 2 bytes
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ANCOUNT | 2 bytes
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| NSCOUNT | 2 bytes
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| ARCOUNT | 2 bytes
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| QUESTION | variable
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+Encoding Labels
Each label is length-prefixed:
get.newyork.weather.public.v1.resolvedb.net
Encodes to:
03 67 65 74 = length(3) + "get"
07 77 65 61 74 68 65 72 = length(7) + "weather"
06 70 75 62 6c 69 63 = length(6) + "public"
02 76 31 = length(2) + "v1"
09 72 65 73 6f 6c 76 65 64 62 = length(9) + "resolvedb"
03 6e 65 74 = length(3) + "net"
00 = null terminatorBase64url Encoding
- Remove
=padding - Replace
+with- - Replace
/with_
function base64urlEncode(buffer) {
return Buffer.from(buffer)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}System Resolver Configuration
Firefox
- Open
about:config - Set
network.trr.modeto3(DoH only) - Set
network.trr.uritohttps://api.resolvedb.io/dns-query
Chrome
- Open
chrome://settings/security - Enable "Use secure DNS"
- Select "With Custom" and enter
https://api.resolvedb.io/dns-query
macOS (Encrypted DNS Profile)
Create a .mobileconfig profile:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadContent</key>
<array>
<dict>
<key>DNSSettings</key>
<dict>
<key>DNSProtocol</key>
<string>HTTPS</string>
<key>ServerURL</key>
<string>https://api.resolvedb.io/dns-query</string>
</dict>
<key>PayloadType</key>
<string>com.apple.dnsSettings.managed</string>
<key>PayloadIdentifier</key>
<string>io.resolvedb.dns</string>
<key>PayloadUUID</key>
<string>A1B2C3D4-E5F6-7890-ABCD-EF1234567890</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</array>
<key>PayloadIdentifier</key>
<string>io.resolvedb.dns.profile</string>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>12345678-90AB-CDEF-1234-567890ABCDEF</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>Linux (systemd-resolved)
Edit /etc/systemd/resolved.conf:
[Resolve]
DNS=api.resolvedb.io
DNSOverTLS=yesRestart: sudo systemctl restart systemd-resolved
Error Handling
HTTP Errors
| Code | Description |
|---|---|
| 200 | Success |
| 400 | Invalid query format or size exceeded |
| 415 | Unsupported media type |
| 429 | Rate limit exceeded |
| 500 | Server error |
DNS Errors
Check the RCODE in the response header:
| RCODE | Name | Description |
|---|---|---|
| 0 | NOERROR | Success |
| 1 | FORMERR | Malformed query |
| 2 | SERVFAIL | Server failure |
| 3 | NXDOMAIN | Name does not exist |
| 5 | REFUSED | Query refused |
Size Limits
| Limit | Value |
|---|---|
| GET query parameter | 8KB (base64 encoded) |
| POST body | 4KB |
| Response | 64KB |
For responses exceeding UDP limits (512 bytes), the response is delivered without truncation over HTTPS.
Rate Limits
Same as JSON API:
| Plan | Requests/min |
|---|---|
| Free | 60 |
| Pro | 600 |
| Enterprise | Custom |
Next Steps
- DoH JSON API - Simpler JSON format for applications
- UQRP Protocol - Query and response format
- Security - DNSSEC and encryption