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

  1. Check the wrapped error to distinguish connection reset vs context deadline vs premature EOF
  2. Retry the request with the library's built-in retry/backoff; transient body-read failures usually succeed on a second attempt
  3. Increase timeouts on the http.Client / request context so large responses are not cut off
  4. Ensure no other code closes resp.Body before ReadAll completes (avoid double-close from stacked defers)
  5. 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

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 expiry

View on GitHub (pinned to c245815e75)