tailscale/tailscale · error

unexpected response Content-Type %q

Error message

unexpected response Content-Type %q

What it means

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.

Source

Thrown at net/tsdial/dohclient.go:85

	if err != nil {
		return 0, err
	}
	const dohType = "application/dns-message"
	req.Header.Set("Content-Type", dohType)
	hc := c.hc
	if hc == nil {
		hc = http.DefaultClient
	}
	hres, err := hc.Do(req)
	if err != nil {
		return 0, err
	}
	defer hres.Body.Close()
	if hres.StatusCode != 200 {
		return 0, errors.New(hres.Status)
	}
	if ct := hres.Header.Get("Content-Type"); ct != dohType {
		return 0, fmt.Errorf("unexpected response Content-Type %q", ct)
	}
	_, err = io.Copy(&c.rbuf, hres.Body)
	if err != nil {
		return 0, err
	}
	if c.dnsCache != nil {
		c.dnsCache.AddCacheEntry(packet, c.rbuf.Bytes())
	}
	return len(packet), nil
}

type todoAddr struct{}

func (todoAddr) Network() string { return "unused" }
func (todoAddr) String() string  { return "unused-todoAddr" }

View on GitHub (pinned to 6e0912f979)

Solutions

  1. 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>
  2. Upgrade both tailnet nodes to the same version so ExitDNS PeerAPI matches
  3. Inspect the response body — an HTML title usually reveals the captive portal or proxy
  4. Bypass intercepting proxies for the DoH host
  5. If you operate the server, fix it to return application/dns-message on the wire path

Example fix

// before: DoH client pointed at the site root
baseURL := "https://dns.example.com"

// after: RFC 8483 wire endpoint
baseURL := "https://dns.example.com/dns-query"
Defensive patterns

Strategy: try-catch

Validate before calling

// preflight: confirm the endpoint speaks RFC 8483 before wiring it as DoH
req, _ := http.NewRequest(http.MethodPost, baseURL, bytes.NewReader(dnsQueryMsg))
req.Header.Set("Content-Type", "application/dns-message")
if res, err := hc.Do(req); err == nil {
	if ct := res.Header.Get("Content-Type"); ct != "application/dns-message" {
		// not a DoH endpoint; fail fast with a clear configuration error
	}
}

Try / catch

if err != nil && strings.Contains(err.Error(), "unexpected response Content-Type") {
	// endpoint is wrong or something is intercepting: do not retry the same URL;
	// check the URL path, then the network path (captive portal / proxy)
}

Prevention

When it happens

Trigger: 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.

Common situations: Exit node and client on different Tailscale versions during upgrades; captive portals; pointing the DoH client at a management URL; corporate proxies rewriting responses.

Related errors


AI-assisted analysis of tailscale/tailscale@6e0912f979 (2026-08-18). Data as JSON: /api/errors/a6deceeb493e9e46. Report an issue: GitHub.