AlexxIT/go2rtc · error
failed to read response body
Error message
failed to read response body: %w
What it means
After a successful HTTP response, RingApi.Request reads the entire body with io.ReadAll(resp.Body). If reading fails (connection reset mid-body, context cancellation, truncated chunked response), it returns 'failed to read response body' wrapping the I/O error. The HTTP exchange succeeded at the status level but the payload could not be retrieved.
Solutions
- Check the wrapped error to distinguish connection reset vs context deadline vs premature EOF
- Retry the request with the library's built-in retry/backoff; transient body-read failures usually succeed on a second attempt
- Increase timeouts on the http.Client / request context so large responses are not cut off
- Ensure no other code closes resp.Body before ReadAll completes (avoid double-close from stacked defers)
- Bypass misbehaving intermediaries (proxy/VPN) that truncate streaming responses
Example fix
// before
responseBody, err = io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
// after
responseBody, err = io.ReadAll(resp.Body)
if err != nil {
if errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, context.DeadlineExceeded) {
return nil, retryable(fmt.Errorf("failed to read response body: %w", err))
}
return nil, fmt.Errorf("failed to read response body: %w", err)
} Defensive patterns
Strategy: retry
Validate before calling
// Go: ensure the client/context allows enough time for the full response
if c.httpClient.Timeout < 30*time.Second {
c.httpClient.Timeout = 30 * time.Second
}
if deadline, ok := ctx.Deadline(); ok && time.Until(deadline) < 10*time.Second {
return errors.New("context deadline too short for ring api response")
} Try / catch
// Go
resp, err := api.Request("GET", url, nil)
if err != nil {
if strings.Contains(err.Error(), "failed to read response body") {
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
resp, err = api.Request("GET", url, nil) // one retry for transient truncation
}
}
if err != nil {
return err
}
} Prevention
- Keep exactly one owner of resp.Body per request; avoid stacked defers that close early
- Set generous but bounded client timeouts for large device/event listings
- Avoid calling over unstable links without application-level retries
- Log the wrapped cause to distinguish resets, truncation, and deadline cancellation
When it happens
Trigger: Calling any RingApi.Request when the response stream breaks: server closes connection mid-transfer, network drop during body read, resp.Body already partially consumed/closed, or a request context deadline expiring mid-read.
Common situations: Flaky Wi-Fi/mobile connections dropping mid-download; proxies or load balancers with short idle timeouts cutting the connection; very large responses over an unstable link; an earlier `defer resp.Body.Close()` double-closing the body in caller-managed flows.
Related errors
AI-assisted analysis of AlexxIT/go2rtc@c245815e75 (2026-09-07).
Data as JSON: /api/errors/1306b6dd7cb28e52.
Report an issue: GitHub.
Appendix: source
Thrown at pkg/ring/api.go:448
// Make request with retries
var resp *http.Response
var responseBody []byte
for attempt := 0; attempt <= maxRetries; attempt++ {
resp, err = c.httpClient.Do(req)
if err != nil {
if attempt == maxRetries {
return nil, fmt.Errorf("request failed after %d retries: %w", maxRetries, err)
}
time.Sleep(5 * time.Second)
continue
}
defer resp.Body.Close()
responseBody, err = io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
// Handle 401 by refreshing auth and retrying
if resp.StatusCode == http.StatusUnauthorized {
// Reset token to force refresh
c.authMutex.Lock()
c.authToken = nil
c.tokenExpiry = time.Time{} // Reset token expiry
c.authMutex.Unlock()
if attempt == maxRetries {
return nil, fmt.Errorf("authentication failed after %d retries", maxRetries)
}
// By 401 with Auth AND Session start over
c.sessionMutex.Lock()
c.session = nil
c.sessionExpiry = time.Time{} // Reset session expiryView on GitHub (pinned to c245815e75)