charmbracelet/crush · error

failed to read response: %w

Error message

failed to read response: %w

What it means

Wraps an io.ReadAll failure while draining the HTTP response body from the DuckDuckGo Lite endpoint in searchDuckDuckGo (internal/agent/tools/search.go:100-103). The request already succeeded with HTTP 200; only reading the body failed. Common wrapped causes are context cancellation/deadline exceeded mid-read, unexpected EOF from a truncated connection, or a proxy closing the connection early.

Source

Thrown at internal/agent/tools/search.go:102

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

	// A 202 from DuckDuckGo is the anomaly-challenge interstitial, not a
	// result page; report throttling rather than parsing it into an
	// empty result set.
	if resp.StatusCode == http.StatusAccepted {
		return nil, errSearchRateLimited
	}
	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("search failed with status code: %d", resp.StatusCode)
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response: %w", err)
	}

	content := string(body)
	for _, marker := range ddgAnomalyMarkers {
		if strings.Contains(content, marker) {
			return nil, errSearchRateLimited
		}
	}

	return parseLiteSearchResults(content, maxResults)
}

func setRandomizedHeaders(req *http.Request) {
	req.Header.Set("User-Agent", userAgents[rand.IntN(len(userAgents))])
	req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
	req.Header.Set("Accept-Language", acceptLanguages[rand.IntN(len(acceptLanguages))])
	req.Header.Set("Accept-Encoding", "identity")
	req.Header.Set("Connection", "keep-alive")

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Retry the search once with a fresh context; transient read interruptions are usually intermittent.
  2. Increase the http.Client timeout or the tool-call timeout so the response body can be fully read.
  3. Check network stability / proxy configuration (HTTP_PROXY, corporate MITM proxies) that can truncate responses.
  4. If persistent, inspect the wrapped error (%w) for context.Canceled vs unexpected EOF to decide between cancelling logic and network fixes.

Example fix

// before
ctx := context.Background()
results, err := searchDuckDuckGo(ctx, client, query, 10)

// after
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
results, err := searchDuckDuckGo(ctx, client, query, 10)
if err != nil && errors.Is(err, context.DeadlineExceeded) {
    // retry once or surface a clearer timeout message
}
Defensive patterns

Strategy: retry

Validate before calling

if err := ctx.Err(); err != nil { return nil, fmt.Errorf("search context already cancelled: %w", err) }

Type guard

var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() { /* treat as retryable timeout */ }

Try / catch

results, err := searchDuckDuckGo(ctx, client, query, 10)
if err != nil {
    if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, io.ErrUnexpectedEOF) {
        // retry once with a longer deadline
    }
    return err
}

Prevention

When it happens

Trigger: io.ReadAll(resp.Body) errors after a 200 response from lite.duckduckgo.com/lite/: context deadline exceeded (Timeout param or client timeout fires mid-download), server closes the connection before the full HTML is sent (unexpected EOF), or a TLS/HTTP2 stream reset occurs during body transfer.

Common situations: Slow or flaky networks and mobile/VPN connections dropping mid-response; a low http.Client.Timeout that cuts off large result pages; corporate proxies or firewalls that truncate long responses; the search context being cancelled by an upstream tool-call timeout.

Related errors


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