{"record":{"id":"859cd57494f9cee3","repo":"gastownhall/beads","slug":"failed-to-read-response-attempt-d-d-w-859cd5","errorCode":null,"errorMessage":"failed to read response (attempt %d/%d): %w","messagePattern":"failed to read response \\(attempt (.+?)/(.+?)\\): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/jira/client.go","lineNumber":381,"sourceCode":"\t\t}\n\n\t\tc.setAuth(req)\n\t\treq.Header.Set(\"Accept\", \"application/json\")\n\t\treq.Header.Set(\"User-Agent\", \"bd-jira-sync/1.0\")\n\t\tif body != nil {\n\t\t\treq.Header.Set(\"Content-Type\", \"application/json\")\n\t\t}\n\n\t\tresp, err := c.HTTPClient.Do(req)\n\t\tif err != nil {\n\t\t\tlastErr = fmt.Errorf(\"request failed (attempt %d/%d): %w\", attempt+1, MaxRetries+1, err)\n\t\t\tcontinue\n\t\t}\n\n\t\trespBody, err := io.ReadAll(io.LimitReader(resp.Body, MaxResponseSize))\n\t\t_ = resp.Body.Close()\n\t\tif err != nil {\n\t\t\tlastErr = fmt.Errorf(\"failed to read response (attempt %d/%d): %w\", attempt+1, MaxRetries+1, err)\n\t\t\tcontinue\n\t\t}\n\n\t\t// PUT returns 204 No Content on success\n\t\tif resp.StatusCode == http.StatusNoContent {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\tif resp.StatusCode >= 200 && resp.StatusCode < 300 {\n\t\t\treturn respBody, nil\n\t\t}\n\n\t\t// Permanent failures — no retry.\n\t\tswitch resp.StatusCode {\n\t\tcase http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound:\n\t\t\treturn nil, fmt.Errorf(\"jira API returned %d: %s\", resp.StatusCode, string(respBody))\n\t\t}\n","sourceCodeStart":363,"sourceCodeEnd":399,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/jira/client.go#L363-L399","documentation":"This error wraps an underlying I/O failure that occurred while reading the Jira HTTP response body in doRequest. The client reads up to MaxResponseSize bytes via io.ReadAll on a LimitReader; any read failure (connection reset mid-body, TLS error, premature EOF) is wrapped with the attempt counter (of MaxRetries+1) and the original error. The request is then retried until attempts are exhausted, after which the last wrapped error surfaces via 'max retries exceeded'.","triggerScenarios":"io.ReadAll(resp.Body) returns a non-nil err during any retry attempt of doRequest — e.g. the server closes the connection mid-body, a proxy drops the stream, or a TLS read fails.","commonSituations":"Corporate proxies or load balancers with short idle timeouts killing long responses; flaky VPN/network; Jira instance (especially self-hosted) restarting under load; response larger than expected causing client/server disagreement.","solutions":["Retry the operation — the client already retries internally; if it fails all attempts, check network stability between client and the Jira host.","Inspect the wrapped cause (%w) at the end of the message to identify the transport-level problem (e.g. 'connection reset by peer', 'unexpected EOF').","Check for proxies/firewalls/load balancers between the client and Jira that terminate connections; raise their idle timeouts.","If self-hosted Jira, check server logs and reverse-proxy (nginx/ALB) timeout settings.","Verify TLS certificates and that the Jira URL scheme (http/https) is correct."],"exampleFix":"// before: seeing 'failed to read response (attempt 3/4): unexpected EOF' with no insight\nresp, _ := client.doRequest(ctx, req)\n// after: log the unwrapped cause to diagnose the transport failure\nif err != nil {\n    var unwrapped error = errors.Unwrap(err)\n    log.Printf(\"jira read failed, cause: %v\", unwrapped)\n}","handlingStrategy":"retry","validationCode":"// Verify reachability before issuing real calls\nreq, _ := http.NewRequestWithContext(ctx, \"GET\", jiraURL+\"/rest/api/3/myself\", nil)\nif _, err := http.DefaultClient.Do(req); err != nil {\n    return fmt.Errorf(\"jira host unreachable, check network/proxy: %w\", err)\n}","typeGuard":null,"tryCatchPattern":"if err != nil {\n    if strings.Contains(err.Error(), \"failed to read response\") || strings.Contains(err.Error(), \"max retries\") {\n        // transient transport issue: back off and retry later\n        time.Sleep(backoff)\n        return retry(op)\n    }\n    return err\n}","preventionTips":["Run bulk workloads on stable network paths; avoid flaky VPNs for long syncs.","Set proxy/LB idle timeouts longer than the largest expected Jira response.","Honor the wrapped cause (%w) in logs to distinguish read failures from status failures.","Keep MaxResponseSize-aware expectations; very large responses over slow links are more read-failure prone."],"tags":["jira","http","network","io","retry"],"backgroundTag":"response-body-read-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}