Problem
Three (soon four with verda) provider clients clone the same Do loop + 429 + backoff + jitter + retry classifier:
internal/sentry/client.go:177-415
internal/linear/client.go:140-291
internal/notion/client.go:118-249
internal/verda/client.go
Subtle divergence has already caused incidents:
- Notion's
isRetryableNetErr (line 240) silently drops "connection refused" + "no such host"; Sentry/Linear keep them as retryable → identical net errors retry on Sentry but bail on Notion
- Linear's API sends
RateLimit-Reset (absolute epoch) but the Linear client only reads Retry-After → bursts retry too soon, contributing to ban storms
Where
Listed above. Look for func (c *Client) Do(, parseRetryWait, sleepWithJitter, isRetryableNetErr.
Fix
Create internal/providerclient with:
type RetryPolicy struct {
MaxRetries int
BaseBackoff time.Duration
MaxBackoff time.Duration
RetryStatuses []int // [429, 502, 503, 504]
}
func Do(ctx context.Context, client *http.Client, req *http.Request, policy RetryPolicy) (*http.Response, []byte, error)
func ParseRetryWait(resp *http.Response, def time.Duration) time.Duration // honors Retry-After, RateLimit-Reset
func IsRetryableNetErr(err error) bool // canonical: timeouts + connreset + EOF, NOT DNS/refused
Each provider keeps its auth-header builder and response decoder. Migrate sentry/linear/notion first; verda last.
Acceptance criteria
internal/providerclient has 100% test coverage on retry logic (429, 5xx, network errors, Retry-After, RateLimit-Reset, context cancel)
- All four providers route their requests through it
- A behavioural test that the same network error is treated identically across all four
Problem
Three (soon four with verda) provider clients clone the same
Doloop + 429 + backoff + jitter + retry classifier:internal/sentry/client.go:177-415internal/linear/client.go:140-291internal/notion/client.go:118-249internal/verda/client.goSubtle divergence has already caused incidents:
isRetryableNetErr(line 240) silently drops "connection refused" + "no such host"; Sentry/Linear keep them as retryable → identical net errors retry on Sentry but bail on NotionRateLimit-Reset(absolute epoch) but the Linear client only readsRetry-After→ bursts retry too soon, contributing to ban stormsWhere
Listed above. Look for
func (c *Client) Do(,parseRetryWait,sleepWithJitter,isRetryableNetErr.Fix
Create
internal/providerclientwith:Each provider keeps its auth-header builder and response decoder. Migrate sentry/linear/notion first; verda last.
Acceptance criteria
internal/providerclienthas 100% test coverage on retry logic (429, 5xx, network errors, Retry-After, RateLimit-Reset, context cancel)