{"record":{"id":"b5d2d51c7f958676","repo":"charmbracelet/crush","slug":"search-failed-with-status-code-d","errorCode":null,"errorMessage":"search failed with status code: %d","messagePattern":"search failed with status code: (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/agent/tools/search.go","lineNumber":97,"sourceCode":"\t\treturn nil, fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\tsetRandomizedHeaders(req)\n\n\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) {","sourceCodeStart":79,"sourceCodeEnd":115,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/agent/tools/search.go#L79-L115","documentation":"DuckDuckGo returned a non-200, non-202 status (e.g. 403 Forbidden, 429 Too Many Requests, 5xx). The tool treats 202 as rate-limit anomaly challenge; every other non-OK status is rejected with this error since the body won't be a parsable result page. It reflects server-side rejection, not a client bug.","triggerScenarios":"searchDuckDuckGo receives resp.StatusCode not in {200, 202} — typically 403 (bot detection/block), 429 (rate limited beyond challenge), or 500-503 (server-side outage).","commonSituations":"Hammering the endpoint from many requests triggers bot-blocking (403); regional blocks; DuckDuckGo serving maintenance errors; very heavy automated usage from one IP.","solutions":["Check the logged status code: 429/403 means back off and slow the request rate","Add jitter/backoff and cache results to reduce request volume","Consider routing through a different network/egress if the IP is blocked","Retry later on 5xx — the failure is server-side"],"exampleFix":"// before\nfor _, q := range queries { search(q) } // tight loop, triggers 403/429\n// after\nfor i, q := range queries {\n    if i > 0 { time.Sleep(2*time.Second + jitter()) }\n    search(q)\n}","handlingStrategy":"retry","validationCode":"// after the call, guard before parsing\nif resp.StatusCode == http.StatusAccepted { /* rate-limited */ }\nif resp.StatusCode != http.StatusOK {\n    return fmt.Errorf(\"search unavailable (status %d), backing off\", resp.StatusCode)\n}","typeGuard":null,"tryCatchPattern":"if errors.Is(err, errSearchRateLimited) || strings.Contains(err.Error(), \"status code: 4\") {\n    select {\n    case <-time.After(backoffWithJitter()):\n        return searchDuckDuckGo(ctx, query) // bounded retry\n    case <-ctx.Done():\n        return ctx.Err()\n    }\n}","preventionTips":["Throttle search requests with rate limiting and jitter","Cache successful results per query","Treat 403/429 as back-off signals, not bugs","Fall back to an alternate search backend for critical paths"],"tags":["http","rate-limit","search"],"backgroundTag":"http-non-200-status","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}