{"record":{"id":"b9696c5e442987a8","repo":"github/github-mcp-server","slug":"failed-to-read-response-body-w-b9696c","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/github/issues.go","lineNumber":731,"sourceCode":"}\n\nfunc GetIssue(ctx context.Context, client *github.Client, deps ToolDependencies, owner string, repo string, issueNumber int) (*mcp.CallToolResult, error) {\n\tcache, err := deps.GetRepoAccessCache(ctx)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get repo access cache: %w\", err)\n\t}\n\tflags := deps.GetFlags(ctx)\n\n\tissue, resp, err := client.Issues.Get(ctx, owner, repo, issueNumber)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to get issue: %w\", err)\n\t}\n\tdefer func() { _ = resp.Body.Close() }()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tbody, 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\t\treturn ghErrors.NewGitHubAPIStatusErrorResponse(ctx, \"failed to get issue\", resp, body), nil\n\t}\n\n\tif flags.LockdownMode {\n\t\tif restricted, err := authorLockdownResult(ctx, cache, owner, repo, issue.GetUser().GetLogin(), lockdownIssueRestrictedMessage); restricted != nil || err != nil {\n\t\t\treturn restricted, err\n\t\t}\n\t}\n\n\t// Sanitize title/body on response\n\tif issue != nil {\n\t\tif issue.Title != nil {\n\t\t\tissue.Title = github.Ptr(sanitize.Sanitize(*issue.Title))\n\t\t}\n\t\tif issue.Body != nil {\n\t\t\tissue.Body = github.Ptr(sanitize.Sanitize(*issue.Body))\n\t\t}","sourceCodeStart":713,"sourceCodeEnd":749,"githubUrl":"https://github.com/github/github-mcp-server/blob/0ea1f775a7c73eff1bd2e25904d01136756bbfe2/pkg/github/issues.go#L713-L749","documentation":"While handling a non-200 response from the GitHub 'get issue' REST call, the server reads resp.Body with io.ReadAll to build a detailed status error via NewGitHubAPIStatusErrorResponse. This error means that read itself failed, so the original HTTP failure status (e.g. 404, 403, 500) is known but GitHub's explanatory body could not be captured. It is a secondary failure that masks the primary API error details.","triggerScenarios":"GET /repos/{owner}/{repo}/issues/{number} returns a non-200 status AND reading the body fails: connection reset after headers were received, keep-alive connection closed mid-body by a proxy/LB, context canceled between status check and ReadAll, or a truncated 502/504 body from a middlebox.","commonSituations":"Aggressive http.Client timeouts shorter than body transfer time, corporate proxies or GLBs truncating error responses, GHES instances behind custom ingress, retry storms during GitHub incidents when large rate-limit error bodies are returned.","solutions":["Retry the get_issue call once with backoff — both the status failure and the body-read failure are typically transient","Increase the HTTP client's overall timeout or use an http.Client with a sane Transport (ResponseHeaderTimeout vs overall Timeout) so the body read is not cut off","Check whether the context was canceled (errors.Is(err, context.Canceled)) before retrying; a canceled context must not be retried","If persistent, reproduce outside the server (curl -v the same endpoint with the token) to identify the middlebox truncating bodies","As a library-level fix, treat the status code as the primary signal and degrade to a body-less status error instead of discarding it"],"exampleFix":"// before\nif resp.StatusCode != http.StatusOK {\n    body, err := io.ReadAll(resp.Body)\n    if err != nil {\n        return nil, fmt.Errorf(\"failed to read response body: %w\", err)\n    }\n    return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, \"failed to get issue\", resp, body), nil\n}\n\n// after — keep the status failure primary; body is best-effort detail\nif resp.StatusCode != http.StatusOK {\n    body, readErr := io.ReadAll(resp.Body)\n    if readErr != nil {\n        deps.Logger(ctx).Warn(\"failed to read error response body\", \"error\", readErr, \"status\", resp.StatusCode)\n    }\n    return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, \"failed to get issue\", resp, body), nil\n}","handlingStrategy":"retry","validationCode":"// Pre-flight: make sure the call has room to read a full error body\nif _, ok := ctx.Deadline(); !ok {\n    ctx, cancel := context.WithTimeout(ctx, 30*time.Second)\n    defer cancel()\n}\nif client.Client == nil || client.BaseURL == nil {\n    return fmt.Errorf(\"github client not initialized\")\n}","typeGuard":"func isResponseBodyReadError(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"failed to read response body\")\n}","tryCatchPattern":"result, err := githubpkg.GetIssue(ctx, client, deps, owner, repo, num)\nif err != nil {\n    if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {\n        return err // do not retry caller-canceled work\n    }\n    if isResponseBodyReadError(err) {\n        return retryOnce(ctx, func() (*mcp.CallToolResult, error) {\n            return githubpkg.GetIssue(ctx, client, deps, owner, repo, num)\n        })\n    }\n    return err\n}","preventionTips":["Configure the shared http.Client with generous ResponseHeaderTimeout rather than a tight overall Timeout","Reuse a single warmed client to avoid keep-alive races on fresh connections","Propagate cancellation instead of background contexts so failures are attributable","Log resp.StatusCode alongside body-read errors — the status usually tells the real story"],"tags":["network","http","response-body","issues"],"backgroundTag":null,"analyzedSha":"0ea1f775a7c73eff1bd2e25904d01136756bbfe2","analyzedAt":"2026-08-15T18:10:19.804Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}