{"record":{"id":"43ec57ed6a966313","repo":"vxcontrol/pentagi","slug":"s-http-d","errorCode":null,"errorMessage":"%s (HTTP %d)","messagePattern":"(.+?) \\(HTTP (.+?)\\)","errorType":"exception","errorClass":"RetryableError","httpStatus":null,"severity":"warning","filePath":"backend/pkg/tools/searchers/errors.go","lineNumber":59,"sourceCode":"\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}\n","sourceCodeStart":41,"sourceCodeEnd":76,"githubUrl":"https://github.com/vxcontrol/pentagi/blob/ea665308baaff015b226f308438a68d929d0f29b/backend/pkg/tools/searchers/errors.go#L41-L76","documentation":"ClassifyHTTPStatus returns a RetryableError for any status >= 500, since an upstream/server-side problem may clear on a retry of the same engine. The message embeds the caller-provided text plus the actual HTTP status code.","triggerScenarios":"Upstream search engine returns 500, 502, 503 or similar while ClassifyHTTPStatus is called from an engine adapter (e.g. classifyGoogleError).","commonSituations":"Engine provider outage or partial degradation, gateway timeouts behind the search API, transient 502s from a CDN in front of the engine.","solutions":["Retry the same engine; the typed error signals the transient nature.","Check the engine provider's status page for an outage.","If 5xx persists, switch to a fallback engine via the fallbackStrategy chain.","Log the status code and msg to identify which upstream endpoint is failing."],"exampleFix":"// before\nreturn classifyGoogleError(status, body)\n// after (caller side)\nif err := ...; searchers.IsRetryable(err) { retryOrFallback(err) }","handlingStrategy":"retry","validationCode":"// health-check the engine before batch runs\nresp, err := http.Get(engineBaseURL + \"/healthz\")\nif err != nil || resp.StatusCode >= 500 { skipEngine() }","typeGuard":"func isServerError(err error) bool {\n  return searchers.IsRetryable(err) && !strings.Contains(err.Error(), \"HTTP 429\")\n}","tryCatchPattern":"err := searcher.Handle(ctx, q)\nif err != nil && searchers.IsRetryable(err) {\n  if attempt < maxAttempts { retryWithBackoff() } else { useFallbackEngine() }\n}","preventionTips":["Subscribe to engine provider status pages.","Cap retries with exponential backoff, then fall back.","Keep a second engine configured for every mode.","Log status codes to spot persistent upstream degradation."],"tags":["http","server-error","retryable","searchers"],"backgroundTag":"http-5xx-server-error","analyzedSha":"ea665308baaff015b226f308438a68d929d0f29b","analyzedAt":"2026-09-01T14:16:31.421Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}