{"record":{"id":"e588a2a5e57cee70","repo":"vxcontrol/pentagi","slug":"s-http-429","errorCode":null,"errorMessage":"%s (HTTP 429)","messagePattern":"(.+?) \\(HTTP 429\\)","errorType":"exception","errorClass":"RetryableError","httpStatus":429,"severity":"warning","filePath":"backend/pkg/tools/searchers/errors.go","lineNumber":57,"sourceCode":"\treturn &RetryableError{Err: err, RetryAfter: retryAfter}\n}\n\n// Fatal wraps err as a FatalError.\nfunc Fatal(err error) error {\n\treturn &FatalError{Err: err}\n}\n\n// ClassifyHTTPStatus maps a non-2xx HTTP status to the right typed error so every\n// engine classifies identically. The engine supplies a human-readable message; the\n// retry/fatal decision is centralized here.\n//\n//\t429       -> RetryableError (rate limited; back off and retry the same engine)\n//\t5xx       -> RetryableError (upstream problem; may clear on retry)\n//\t4xx/other -> FatalError     (auth/bad-request/etc.; retrying the same engine is pointless)\nfunc ClassifyHTTPStatus(status int, msg string) error {\n\tswitch {\n\tcase status == http.StatusTooManyRequests:\n\t\treturn Retryable(fmt.Errorf(\"%s (HTTP 429)\", msg), 0)\n\tcase status >= 500:\n\t\treturn Retryable(fmt.Errorf(\"%s (HTTP %d)\", msg, status), 0)\n\tdefault:\n\t\treturn Fatal(fmt.Errorf(\"%s (HTTP %d)\", msg, status))\n\t}\n}\n\n// IsRetryable reports whether err (or anything it wraps) is a RetryableError.\nfunc IsRetryable(err error) bool {\n\tvar t *RetryableError\n\treturn errors.As(err, &t)\n}\n\n// IsFatal reports whether err (or anything it wraps) is a FatalError.\nfunc IsFatal(err error) bool {\n\tvar t *FatalError\n\treturn errors.As(err, &t)\n}","sourceCodeStart":39,"sourceCodeEnd":75,"githubUrl":"https://github.com/vxcontrol/pentagi/blob/ea665308baaff015b226f308438a68d929d0f29b/backend/pkg/tools/searchers/errors.go#L39-L75","documentation":"ClassifyHTTPStatus maps an HTTP status code from a search backend into a typed searcher error. HTTP 429 means the engine rate-limited the request, so the function returns a RetryableError, telling the orchestrator the same engine may succeed after backing off (retryAfter=0 means immediate/secondary retry).","triggerScenarios":"Any searcher (e.g. Google via classifyGoogleError) receives HTTP 429 from the upstream engine API during a web_search call.","commonSituations":"Exhausted per-key query quotas on Google/Firecrawl-style APIs, many flows sharing one API key, bursty agent traffic hitting free-tier limits.","solutions":["Back off and retry the same engine after a delay (the error is marked retryable by design).","Reduce request rate or add jitter/caching between searches in the flow.","Use a higher-tier plan or rotate to a different API key for the engine.","Configure fallback engines so the orchestrator can switch when one is rate-limited."],"exampleFix":"// before: hammering the engine in a loop\nfor _, q := range queries { s.Handle(ctx, q) }\n// after: respect retryability\nerr := s.Handle(ctx, q)\nif searchers.IsRetryable(err) { time.Sleep(backoff); retry() } else if err != nil { fallbackEngine() }","handlingStrategy":"retry","validationCode":"// preflight quota probe\nresp, _ := http.Head(\"https://api.firecrawl.dev\")\nif resp != nil && resp.StatusCode == 429 { time.Sleep(backoff) }","typeGuard":"func isRateLimited(err error) bool {\n  var r *searchers.RetryableError\n  return errors.As(err, &r) && strings.Contains(err.Error(), \"HTTP 429\")\n}","tryCatchPattern":"if err := searcher.Handle(ctx, q); err != nil {\n  if searchers.IsRetryable(err) { time.Sleep(backoff); go retry(q) } else { log.Error(err) }\n}","preventionTips":["Respect per-engine rate limits; add client-side throttling/jitter.","Cache repeated queries to reduce engine calls.","Rotate or upgrade API keys before quotas are hit.","Configure multiple fallback engines in web_search fallbackStrategy."],"tags":["http","rate-limit","retryable","searchers"],"backgroundTag":"http-429-rate-limited","analyzedSha":"ea665308baaff015b226f308438a68d929d0f29b","analyzedAt":"2026-09-01T14:16:31.421Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}