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

  1. Retry the get_issue call once with backoff — both the status failure and the body-read failure are typically transient
  2. 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
  3. Check whether the context was canceled (errors.Is(err, context.Canceled)) before retrying; a canceled context must not be retried
  4. If persistent, reproduce outside the server (curl -v the same endpoint with the token) to identify the middlebox truncating bodies
  5. 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

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


AI-assisted analysis of github/github-mcp-server@0ea1f775a7 (2026-08-15). Data as JSON: /api/errors/b9696c5e442987a8. Report an issue: GitHub.