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

  1. Increase the ExchangerOption timeout — the http.Client Timeout covers body reading too, so a too-short value surfaces here
  2. Retry the Exchange() call with backoff — transient connection resets are common on unstable networks
  3. Check for context cancellation and give the exchange a longer/deeper deadline
  4. Test the endpoint directly (curl the DoH URL) to see if the server/proxy truncates responses
  5. 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

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


AI-assisted analysis of ginuerzh/gost@a33fdbf4c9 (2026-09-02). Data as JSON: /api/errors/79ed9710733e7ba1. Report an issue: GitHub.