{"record":{"id":"a6deceeb493e9e46","repo":"tailscale/tailscale","slug":"unexpected-response-content-type-q-a6dece","errorCode":null,"errorMessage":"unexpected response Content-Type %q","messagePattern":"unexpected response Content-Type %q","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"net/tsdial/dohclient.go","lineNumber":85,"sourceCode":"\tif err != nil {\n\t\treturn 0, err\n\t}\n\tconst dohType = \"application/dns-message\"\n\treq.Header.Set(\"Content-Type\", dohType)\n\thc := c.hc\n\tif hc == nil {\n\t\thc = http.DefaultClient\n\t}\n\thres, err := hc.Do(req)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tdefer hres.Body.Close()\n\tif hres.StatusCode != 200 {\n\t\treturn 0, errors.New(hres.Status)\n\t}\n\tif ct := hres.Header.Get(\"Content-Type\"); ct != dohType {\n\t\treturn 0, fmt.Errorf(\"unexpected response Content-Type %q\", ct)\n\t}\n\t_, err = io.Copy(&c.rbuf, hres.Body)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tif c.dnsCache != nil {\n\t\tc.dnsCache.AddCacheEntry(packet, c.rbuf.Bytes())\n\t}\n\treturn len(packet), nil\n}\n\ntype todoAddr struct{}\n\nfunc (todoAddr) Network() string { return \"unused\" }\nfunc (todoAddr) String() string  { return \"unused-todoAddr\" }\n","sourceCodeStart":67,"sourceCodeEnd":101,"githubUrl":"https://github.com/tailscale/tailscale/blob/6e0912f97994f927632b34ae9e63b53d6516a6ac/net/tsdial/dohclient.go#L67-L101","documentation":"The DoH client in tsdial (used to send DNS over PeerAPI to an exit node's ExitDNS DoH proxy, or to any configured DoH base URL) got an HTTP 200 whose Content-Type is not \"application/dns-message\" (net/tsdial/dohclient.go:84-86). The endpoint answered, but not with a DNS-over-HTTPS wire response — usually an HTML error/redirect page or a non-DoH service behind that URL.","triggerScenarios":"baseURL points at a path that returns text/html (site root instead of /dns-query); a captive portal or filtering proxy answering 200 with HTML; version skew where the exit node's PeerAPI does not implement the DoH endpoint; middleboxes rewriting Content-Type.","commonSituations":"Exit node and client on different Tailscale versions during upgrades; captive portals; pointing the DoH client at a management URL; corporate proxies rewriting responses.","solutions":["Verify the URL is a real DoH endpoint: curl -s -o /dev/null -w '%{content_type}' -H 'content-type: application/dns-message' --data-binary @q.bin <url>","Upgrade both tailnet nodes to the same version so ExitDNS PeerAPI matches","Inspect the response body — an HTML title usually reveals the captive portal or proxy","Bypass intercepting proxies for the DoH host","If you operate the server, fix it to return application/dns-message on the wire path"],"exampleFix":"// before: DoH client pointed at the site root\nbaseURL := \"https://dns.example.com\"\n\n// after: RFC 8483 wire endpoint\nbaseURL := \"https://dns.example.com/dns-query\"","handlingStrategy":"try-catch","validationCode":"// preflight: confirm the endpoint speaks RFC 8483 before wiring it as DoH\nreq, _ := http.NewRequest(http.MethodPost, baseURL, bytes.NewReader(dnsQueryMsg))\nreq.Header.Set(\"Content-Type\", \"application/dns-message\")\nif res, err := hc.Do(req); err == nil {\n\tif ct := res.Header.Get(\"Content-Type\"); ct != \"application/dns-message\" {\n\t\t// not a DoH endpoint; fail fast with a clear configuration error\n\t}\n}","typeGuard":null,"tryCatchPattern":"if err != nil && strings.Contains(err.Error(), \"unexpected response Content-Type\") {\n\t// endpoint is wrong or something is intercepting: do not retry the same URL;\n\t// check the URL path, then the network path (captive portal / proxy)\n}","preventionTips":["Point DoH clients at known wire endpoints (path /dns-query), never site roots","Keep exit node and client on the same tailscale version","Inspect response bodies when DoH behaves oddly — HTML means interception or wrong URL"],"tags":["dns","doh","http","configuration","network","go","tailscale"],"backgroundTag":"unexpected-response-content-type","analyzedSha":"6e0912f97994f927632b34ae9e63b53d6516a6ac","analyzedAt":"2026-08-18T08:17:25.280Z","contentChangedAt":"2026-08-18T08:17:25.280Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}