charmbracelet/crush · error
failed to read response body: %w
Error message
failed to read response body: %w
What it means
Reading the response body (bounded by a 5MB LimitReader) failed with an I/O error. This indicates the connection broke mid-transfer or the server terminated the stream early. The 5MB limit itself does not cause this error — only genuine read failures do.
Source
Thrown at internal/agent/tools/fetch_helpers.go:49
// Use realistic browser headers for better compatibility.
req.Header.Set("User-Agent", BrowserUserAgent)
req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
req.Header.Set("Accept-Language", "en-US,en;q=0.5")
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("failed to fetch URL: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("request failed with status code: %d", resp.StatusCode)
}
maxSize := int64(5 * 1024 * 1024) // 5MB
body, err := io.ReadAll(io.LimitReader(resp.Body, maxSize))
if err != nil {
return "", fmt.Errorf("failed to read response body: %w", err)
}
content := string(body)
if !utf8.ValidString(content) {
return "", errors.New("response content is not valid UTF-8")
}
contentType := resp.Header.Get("Content-Type")
// Convert HTML to markdown for better AI processing.
if strings.Contains(contentType, "text/html") {
// Remove noisy elements before conversion.
cleanedHTML := removeNoisyElements(content)
markdown, err := ConvertHTMLToMarkdown(cleanedHTML)
if err != nil {
return "", fmt.Errorf("failed to convert HTML to markdown: %w", err)
}View on GitHub (pinned to 7944b8e522)
Solutions
- Retry the fetch — transient connection resets are the most common cause
- Verify with curl whether the URL serves a complete response consistently
- Check for proxy/CDN body-size or time limits that truncate transfers
- If the body may exceed 5MB, the LimitReader silently truncates — that is separate; genuine read errors need a retry
Example fix
// before
body, err := io.ReadAll(io.LimitReader(resp.Body, maxSize))
if err != nil {
return "", fmt.Errorf("failed to read response body: %w", err)
}
// after
body, err := io.ReadAll(io.LimitReader(resp.Body, maxSize))
if err != nil {
if errors.Is(err, io.ErrUnexpectedEOF) {
return "", fmt.Errorf("connection closed mid-transfer, retry fetch: %w", err)
}
return "", fmt.Errorf("failed to read response body: %w", err)
} Defensive patterns
Strategy: retry
Validate before calling
null
Type guard
if errors.Is(err, io.ErrUnexpectedEOF) || errors.Is(err, syscall.ECONNRESET) {
// truncated transfer, retry
} Try / catch
body, err := io.ReadAll(io.LimitReader(resp.Body, maxSize))
if err != nil {
return "", fmt.Errorf("failed to read response body: %w", err)
} Prevention
- Retry transient body-read failures with backoff
- Keep the LimitReader cap to avoid OOM on huge responses
- Check network stability/proxy timeouts for recurring truncation
- Validate content (e.g. UTF-8) after read, as the code already does
When it happens
Trigger: io.ReadAll(io.LimitReader(resp.Body, 5MB)) returns an error: connection reset by peer, unexpected EOF, TLS record failure mid-body, or server closing the connection prematurely.
Common situations: Unstable networks or mobile connections, proxies that kill long transfers, very large responses on flaky CDNs, keep-alive connections dropped by load balancers mid-response.
Related errors
- failed to write file: %w
- failed to read response: %w
- failed to read response body: %w
- empty providers list from catwalk
- lost connection to the crush server
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/a1ddbbcdc9aa0321.
Report an issue: GitHub.