{"record":{"id":"2b93322bccdf43fa","repo":"sipeed/picoclaw","slug":"w-w","errorCode":null,"errorMessage":"%w: %w","messagePattern":"%w: %w","errorType":"http","errorClass":"ErrRateLimit","httpStatus":429,"severity":"warning","filePath":"pkg/channels/errutil.go","lineNumber":14,"sourceCode":"package channels\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n)\n\n// ClassifySendError wraps a raw error with the appropriate sentinel based on\n// an HTTP status code. Channels that perform HTTP API calls should use this\n// in their Send path.\nfunc ClassifySendError(statusCode int, rawErr error) error {\n\tswitch {\n\tcase statusCode == http.StatusTooManyRequests:\n\t\treturn fmt.Errorf(\"%w: %w\", ErrRateLimit, rawErr)\n\tcase statusCode >= 500:\n\t\treturn fmt.Errorf(\"%w: %w\", ErrTemporary, rawErr)\n\tcase statusCode >= 400:\n\t\treturn fmt.Errorf(\"%w: %w\", ErrSendFailed, rawErr)\n\tdefault:\n\t\treturn rawErr\n\t}\n}\n\n// ClassifyNetError wraps a network/timeout error as ErrTemporary.\nfunc ClassifyNetError(err error) error {\n\tif err == nil {\n\t\treturn nil\n\t}\n\treturn fmt.Errorf(\"%w: %w\", ErrTemporary, err)\n}\n","sourceCodeStart":1,"sourceCodeEnd":31,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/channels/errutil.go#L1-L31","documentation":"channels.ClassifySendError maps HTTP 429 to the ErrRateLimit sentinel, double-wrapping it with the raw platform error (message renders as 'rate limited: <raw>'). Channels doing HTTP API sends call this from their Send path; the manager then waits a fixed delay and retries, unlike ErrTemporary's exponential backoff.","triggerScenarios":"Any channel Send path calling ClassifySendError(statusCode, rawErr) where the platform returned 429 — bursting messages, group chats with heavy traffic, several channels sharing one token, per-route method limits (e.g. Slack/Telegram).","commonSituations":"Bulk notifications, agent storms replying to many chats at once, multiple bot instances on one credential, no client-side throttling.","solutions":["Match it in callers with errors.Is(err, channels.ErrRateLimit) and back off (honor the platform's Retry-After header when present)","Add client-side rate limiting/throttling before the HTTP call so 429s stop occurring","Spread load across allowed tokens/channels or slow the send loop","Keep the wrap as-is so errors.Is keeps matching the sentinel"],"exampleFix":"// before\nresp, err := doSend(req)\nreturn err // callers cannot distinguish 429 from other failures\n\n// after\nresp, err := doSend(req)\nif err == nil && resp.StatusCode == http.StatusTooManyRequests {\n    return channels.ClassifySendError(resp.StatusCode, fmt.Errorf(\"body: %s\", body))\n}","handlingStrategy":"retry","validationCode":"// Client-side throttle before the HTTP call to avoid 429s\nlimiter := rate.NewLimiter(rate.Every(time.Second/5), 5) // example: 5 msg/s\nif err := limiter.Wait(ctx); err != nil {\n    return err\n}","typeGuard":"func isRateLimited(err error) bool { return errors.Is(err, channels.ErrRateLimit) }","tryCatchPattern":"if err := ch.Send(ctx, msg); err != nil {\n    if errors.Is(err, channels.ErrRateLimit) {\n        time.Sleep(retryAfterFromBody(err)) // fixed delay; honor Retry-After when available\n        return ch.Send(ctx, msg)\n    }\n    return err\n}","preventionTips":["Throttle outbound sends client-side before hitting the platform's 429","Honor Retry-After headers when the platform provides them","Always match with errors.Is(err, channels.ErrRateLimit) — the sentinel survives the double-%w wrap","Distribute bursts across time; avoid fan-out replies to many chats in one tick"],"tags":["rate-limit","http-429","retry","errutil","send"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-16T03:17:38.424Z"}