github/github-mcp-server · error
failed to read response body: %w
Error message
failed to read response body: %w
What it means
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.
Source
Thrown at pkg/github/issues.go:731
}
func GetIssue(ctx context.Context, client *github.Client, deps ToolDependencies, owner string, repo string, issueNumber int) (*mcp.CallToolResult, error) {
cache, err := deps.GetRepoAccessCache(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get repo access cache: %w", err)
}
flags := deps.GetFlags(ctx)
issue, resp, err := client.Issues.Get(ctx, owner, repo, issueNumber)
if err != nil {
return nil, fmt.Errorf("failed to get issue: %w", err)
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get issue", resp, body), nil
}
if flags.LockdownMode {
if restricted, err := authorLockdownResult(ctx, cache, owner, repo, issue.GetUser().GetLogin(), lockdownIssueRestrictedMessage); restricted != nil || err != nil {
return restricted, err
}
}
// Sanitize title/body on response
if issue != nil {
if issue.Title != nil {
issue.Title = github.Ptr(sanitize.Sanitize(*issue.Title))
}
if issue.Body != nil {
issue.Body = github.Ptr(sanitize.Sanitize(*issue.Body))
}View on GitHub (pinned to 0ea1f775a7)
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
Example fix
// before
if resp.StatusCode != http.StatusOK {
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get issue", resp, body), nil
}
// after — keep the status failure primary; body is best-effort detail
if resp.StatusCode != http.StatusOK {
body, readErr := io.ReadAll(resp.Body)
if readErr != nil {
deps.Logger(ctx).Warn("failed to read error response body", "error", readErr, "status", resp.StatusCode)
}
return ghErrors.NewGitHubAPIStatusErrorResponse(ctx, "failed to get issue", resp, body), nil
} Defensive patterns
Strategy: retry
Validate before calling
// Pre-flight: make sure the call has room to read a full error body
if _, ok := ctx.Deadline(); !ok {
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
}
if client.Client == nil || client.BaseURL == nil {
return fmt.Errorf("github client not initialized")
} Type guard
func isResponseBodyReadError(err error) bool {
return err != nil && strings.Contains(err.Error(), "failed to read response body")
} Try / catch
result, err := githubpkg.GetIssue(ctx, client, deps, owner, repo, num)
if err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return err // do not retry caller-canceled work
}
if isResponseBodyReadError(err) {
return retryOnce(ctx, func() (*mcp.CallToolResult, error) {
return githubpkg.GetIssue(ctx, client, deps, owner, repo, num)
})
}
return err
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- installation token request failed: %s (reading response: %w)
- decoding installation token response: %w
- failed to download logs: %w
- failed to read response body: %w
- failed to get issue comments: %w
AI-assisted analysis of github/github-mcp-server@0ea1f775a7 (2026-08-15).
Data as JSON: /api/errors/b9696c5e442987a8.
Report an issue: GitHub.