{"record":{"id":"1306b6dd7cb28e52","repo":"AlexxIT/go2rtc","slug":"failed-to-read-response-body-w","errorCode":null,"errorMessage":"failed to read response body: %w","messagePattern":"failed to read response body: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/ring/api.go","lineNumber":448,"sourceCode":"\n\t// Make request with retries\n\tvar resp *http.Response\n\tvar responseBody []byte\n\n\tfor attempt := 0; attempt <= maxRetries; attempt++ {\n\t\tresp, err = c.httpClient.Do(req)\n\t\tif err != nil {\n\t\t\tif attempt == maxRetries {\n\t\t\t\treturn nil, fmt.Errorf(\"request failed after %d retries: %w\", maxRetries, err)\n\t\t\t}\n\t\t\ttime.Sleep(5 * time.Second)\n\t\t\tcontinue\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\tresponseBody, err = io.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to read response body: %w\", err)\n\t\t}\n\n\t\t// Handle 401 by refreshing auth and retrying\n\t\tif resp.StatusCode == http.StatusUnauthorized {\n\t\t\t// Reset token to force refresh\n\t\t\tc.authMutex.Lock()\n\t\t\tc.authToken = nil\n\t\t\tc.tokenExpiry = time.Time{} // Reset token expiry\n\t\t\tc.authMutex.Unlock()\n\n\t\t\tif attempt == maxRetries {\n\t\t\t\treturn nil, fmt.Errorf(\"authentication failed after %d retries\", maxRetries)\n\t\t\t}\n\n\t\t\t// By 401 with Auth AND Session start over\n\t\t\tc.sessionMutex.Lock()\n\t\t\tc.session = nil\n\t\t\tc.sessionExpiry = time.Time{} // Reset session expiry","sourceCodeStart":430,"sourceCodeEnd":466,"githubUrl":"https://github.com/AlexxIT/go2rtc/blob/c245815e75e2a5fd60b4290f12bfc04e55a984d3/pkg/ring/api.go#L430-L466","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nresponseBody, err = io.ReadAll(resp.Body)\nif err != nil {\n    return nil, fmt.Errorf(\"failed to read response body: %w\", err)\n}\n// after\nresponseBody, err = io.ReadAll(resp.Body)\nif err != nil {\n    if errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, context.DeadlineExceeded) {\n        return nil, retryable(fmt.Errorf(\"failed to read response body: %w\", err))\n    }\n    return nil, fmt.Errorf(\"failed to read response body: %w\", err)\n}","handlingStrategy":"retry","validationCode":"// Go: ensure the client/context allows enough time for the full response\nif c.httpClient.Timeout < 30*time.Second {\n    c.httpClient.Timeout = 30 * time.Second\n}\nif deadline, ok := ctx.Deadline(); ok && time.Until(deadline) < 10*time.Second {\n    return errors.New(\"context deadline too short for ring api response\")\n}","typeGuard":null,"tryCatchPattern":"// Go\nresp, err := api.Request(\"GET\", url, nil)\nif err != nil {\n    if strings.Contains(err.Error(), \"failed to read response body\") {\n        var netErr net.Error\n        if errors.As(err, &netErr) && netErr.Timeout() {\n            resp, err = api.Request(\"GET\", url, nil) // one retry for transient truncation\n        }\n    }\n    if err != nil {\n        return err\n    }\n}","preventionTips":["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"],"tags":["network","http","response-body","io"],"backgroundTag":"response-body-read-failed","analyzedSha":"c245815e75e2a5fd60b4290f12bfc04e55a984d3","analyzedAt":"2026-09-07T11:47:02.965Z","contentChangedAt":"2026-09-07T11:47:02.965Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}