charmbracelet/crush · error

failed to read response body: %w

Error message

failed to read response body: %w

What it means

Wraps an io.ReadAll error while reading a successful (HTTP 200) response body from the Sourcegraph GraphQL API (internal/agent/tools/sourcegraph.go:137-140). The status was OK but the body stream broke mid-read — typically context cancellation, an unexpected EOF from a dropped connection, or an HTTP/2 stream error.

Source

Thrown at internal/agent/tools/sourcegraph.go:139

			req.Header.Set("User-Agent", "crush/1.0")

			resp, err := client.Do(req)
			if err != nil {
				return fantasy.ToolResponse{}, fmt.Errorf("failed to fetch URL: %w", err)
			}
			defer resp.Body.Close()

			if resp.StatusCode != http.StatusOK {
				body, _ := io.ReadAll(resp.Body)
				if len(body) > 0 {
					return fantasy.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d, response: %s", resp.StatusCode, string(body))), nil
				}

				return fantasy.NewTextErrorResponse(fmt.Sprintf("Request failed with status code: %d", resp.StatusCode)), nil
			}
			body, err := io.ReadAll(resp.Body)
			if err != nil {
				return fantasy.ToolResponse{}, fmt.Errorf("failed to read response body: %w", err)
			}

			var result map[string]any
			if err = json.Unmarshal(body, &result); err != nil {
				return fantasy.ToolResponse{}, fmt.Errorf("failed to unmarshal response: %w", err)
			}

			formattedResults, err := formatSourcegraphResults(result, params.ContextWindow, params.Count)
			if err != nil {
				return fantasy.NewTextErrorResponse("Failed to format results: " + err.Error()), nil
			}

			return fantasy.NewTextResponse(formattedResults), nil
		},
	)
}

func formatSourcegraphResults(result map[string]any, contextWindow, maxResults int) (string, error) {

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Narrow the query (add repo:, file:, or count:) so the response payload is smaller and completes before timeouts.
  2. Increase the http.Client timeout and/or the tool Timeout param for large searches.
  3. Retry once on transient errors; check errors.Is(err, context.DeadlineExceeded) to distinguish timeouts from dropped connections.
  4. Investigate proxy/VPN stability if unexpected EOF recurs on large responses.

Example fix

// before
params := SourcegraphParams{Query: "fmt.Errorf", Timeout: 5}

// after
params := SourcegraphParams{Query: "fmt.Errorf repo:github.com/charmbracelet/crush", Timeout: 60}
Defensive patterns

Strategy: retry

Validate before calling

if params.Timeout > 0 && params.Timeout < 15 {
    params.Timeout = 15 // avoid cutting large bodies off mid-read
}

Type guard

if errors.Is(err, io.ErrUnexpectedEOF) { /* connection dropped mid-body, retryable */ }
if errors.Is(err, context.DeadlineExceeded) { /* body too large for timeout */ }

Try / catch

body, err := io.ReadAll(resp.Body)
if err != nil {
    if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, io.ErrUnexpectedEOF) {
        // retry once with narrower query / longer timeout
    }
    return err
}

Prevention

When it happens

Trigger: io.ReadAll(resp.Body) errors after a 200 from sourcegraph.com/.api/graphql: the request context deadline fires mid-body, the server or an intermediary closes the connection early (unexpected EOF), or a large result set exceeds what the network can deliver before a timeout.

Common situations: Broad queries (e.g. searching a pattern across all of GitHub) returning huge GraphQL payloads on slow links; flaky VPN/corporate proxies truncating large responses; Timeout param set so low that a big body cannot finish transferring within the client's 30s default.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/e7fdc089b0007233. Report an issue: GitHub.