ginuerzh/gost · error
failed to read the response body: %s
Error message
failed to read the response body: %s
What it means
The DoH server returned 200 but reading the response body with io.ReadAll failed (resolver.go:919). The 200-status wire-format payload could not be fully received — typically the connection was cut or timed out mid-body, so the DNS response bytes are unavailable.
Source
Thrown at resolver.go:919
client := ex.client
if client == nil {
client = http.DefaultClient
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to perform an HTTPS request: %s", err)
}
// Check response status code
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("returned status code %d", resp.StatusCode)
}
// Read wireformat response from the body
buf, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read the response body: %s", err)
}
return buf, nil
}
View on GitHub (pinned to a33fdbf4c9)
Solutions
- Increase the ExchangerOption timeout — the http.Client Timeout covers body reading too, so a too-short value surfaces here
- Retry the Exchange() call with backoff — transient connection resets are common on unstable networks
- Check for context cancellation and give the exchange a longer/deeper deadline
- Test the endpoint directly (curl the DoH URL) to see if the server/proxy truncates responses
- Bypass or reconfigure intermediaries (proxies, VPNs) that cut connections
Example fix
// before
exchanger := NewDoHExchanger(endpoint, nil) // default timeout, flaky link
buf, err := exchanger.Exchange(ctx, query) // failed to read the response body: ...unexpected EOF
// after
exchanger := NewDoHExchanger(endpoint, nil, WithTimeout(15*time.Second))
for i := 0; i < 3; i++ { // retry transient body-read failures
buf, err = exchanger.Exchange(ctx, query)
if err == nil { break }
time.Sleep(time.Duration(1<<i) * 100 * time.Millisecond)
} Defensive patterns
Strategy: retry
Validate before calling
if timeout <= 0 || timeout > 30*time.Second {
// body reads are covered by the client Timeout; pick a safe value
timeout = 15 * time.Second
} Type guard
func isBodyReadFailure(err error) bool {
return err != nil && strings.Contains(err.Error(), "failed to read the response body:")
} Try / catch
var buf []byte
var err error
for attempt := 0; attempt < 3; attempt++ {
buf, err = exchanger.Exchange(ctx, query)
if err == nil { break }
if isBodyReadFailure(err) {
select {
case <-time.After(time.Duration(1<<attempt) * 100 * time.Millisecond):
continue
case <-ctx.Done():
return ctx.Err()
}
}
break
} Prevention
- Set the exchanger timeout large enough to cover slow-link body reads
- Always retry Exchange on transient IO errors — DNS queries are idempotent
- Respect context cancellation in retry loops
- Avoid routing DoH through unreliable intermediaries/proxies
- Watch server-side (or LB) read-timeout settings against your client timeout
When it happens
Trigger: Exchange() on a dohExchanger where the server closes the connection before sending the complete body, the http.Client Timeout fires while reading the body (timeout covers the entire exchange including body read), the context is cancelled mid-read, or a proxy/TLB truncates the response.
Common situations: Flaky mobile/satellite networks dropping connections mid-response; DoH server or load balancer with aggressive idle/read timeouts; http.Client timeout too short for large responses or slow links; intermediaries (corporate proxies) killing long-lived connections.
Related errors
- failed to perform an HTTPS request: %s
- returned status code %d
- failed to create an HTTPS request: %s
- resolver: domain %s does not exists
- connection queue is full
AI-assisted analysis of ginuerzh/gost@a33fdbf4c9 (2026-09-02).
Data as JSON: /api/errors/79ed9710733e7ba1.
Report an issue: GitHub.