{"record":{"id":"1519f34f08fd7762","repo":"charmbracelet/crush","slug":"failed-to-read-response-w","errorCode":null,"errorMessage":"failed to read response: %w","messagePattern":"failed to read response: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/agent/tools/search.go","lineNumber":102,"sourceCode":"\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute search: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\t// A 202 from DuckDuckGo is the anomaly-challenge interstitial, not a\n\t// result page; report throttling rather than parsing it into an\n\t// empty result set.\n\tif resp.StatusCode == http.StatusAccepted {\n\t\treturn nil, errSearchRateLimited\n\t}\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"search failed with status code: %d\", resp.StatusCode)\n\t}\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read response: %w\", err)\n\t}\n\n\tcontent := string(body)\n\tfor _, marker := range ddgAnomalyMarkers {\n\t\tif strings.Contains(content, marker) {\n\t\t\treturn nil, errSearchRateLimited\n\t\t}\n\t}\n\n\treturn parseLiteSearchResults(content, maxResults)\n}\n\nfunc setRandomizedHeaders(req *http.Request) {\n\treq.Header.Set(\"User-Agent\", userAgents[rand.IntN(len(userAgents))])\n\treq.Header.Set(\"Accept\", \"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\")\n\treq.Header.Set(\"Accept-Language\", acceptLanguages[rand.IntN(len(acceptLanguages))])\n\treq.Header.Set(\"Accept-Encoding\", \"identity\")\n\treq.Header.Set(\"Connection\", \"keep-alive\")","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/agent/tools/search.go#L84-L120","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Retry the search once with a fresh context; transient read interruptions are usually intermittent.","Increase the http.Client timeout or the tool-call timeout so the response body can be fully read.","Check network stability / proxy configuration (HTTP_PROXY, corporate MITM proxies) that can truncate responses.","If persistent, inspect the wrapped error (%w) for context.Canceled vs unexpected EOF to decide between cancelling logic and network fixes."],"exampleFix":"// before\nctx := context.Background()\nresults, err := searchDuckDuckGo(ctx, client, query, 10)\n\n// after\nctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)\ndefer cancel()\nresults, err := searchDuckDuckGo(ctx, client, query, 10)\nif err != nil && errors.Is(err, context.DeadlineExceeded) {\n    // retry once or surface a clearer timeout message\n}","handlingStrategy":"retry","validationCode":"if err := ctx.Err(); err != nil { return nil, fmt.Errorf(\"search context already cancelled: %w\", err) }","typeGuard":"var netErr net.Error\nif errors.As(err, &netErr) && netErr.Timeout() { /* treat as retryable timeout */ }","tryCatchPattern":"results, err := searchDuckDuckGo(ctx, client, query, 10)\nif err != nil {\n    if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, io.ErrUnexpectedEOF) {\n        // retry once with a longer deadline\n    }\n    return err\n}","preventionTips":["Give searches a generous context deadline (>= 30s).","Avoid routing through unstable proxies or VPNs for outbound HTTP.","Check ctx.Err() before issuing the request to fail fast on already-cancelled contexts.","Log the wrapped cause with errors.Is/errors.As rather than the outer message."],"tags":["network","http","io","go"],"backgroundTag":"response-body-read-failed","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}