diff --git a/try.go b/try.go index 8f6c91a..a2c0ba1 100644 --- a/try.go +++ b/try.go @@ -356,7 +356,8 @@ func shouldRetry(ctx context.Context, cfg *Config, err error) bool { func calculateNextDelay(cfg *Config, attempt int, err error) time.Duration { // 1. Check for Retry-After override — takes precedence over everything. - if ra, ok := err.(RetryAfterer); ok { + var ra RetryAfterer + if errors.As(err, &ra) { d := ra.RetryAfter() if d > cfg.MaxDelay { return cfg.MaxDelay diff --git a/try_test.go b/try_test.go index bbd0517..66a5fc5 100644 --- a/try_test.go +++ b/try_test.go @@ -101,6 +101,13 @@ type retryAfterError struct{ d time.Duration } func (e retryAfterError) Error() string { return "retry after" } func (e retryAfterError) RetryAfter() time.Duration { return e.d } +type wrappedRetryAfterError struct { + retryAfterError + err error +} + +func (e wrappedRetryAfterError) Unwrap() error { return e.err } + func TestDo_RetryAfter(t *testing.T) { ctx := context.Background() clk := &testClock{afterChan: make(chan time.Time)} @@ -126,6 +133,65 @@ func TestDo_RetryAfter(t *testing.T) { } } +func TestDo_RetryAfterWrapped(t *testing.T) { + const requestedDelay = 20 * time.Millisecond + const maxDelay = 50 * time.Millisecond + const fallbackDelay = time.Millisecond + raErr := retryAfterError{d: requestedDelay} + cappedErr := retryAfterError{d: time.Hour} + unrelatedErr := errors.New("unrelated failure") + + tests := []struct { + name string + err error + want time.Duration + }{ + {"direct", raErr, requestedDelay}, + {"outer retry after", wrappedRetryAfterError{retryAfterError{d: maxDelay}, raErr}, maxDelay}, + {"wrapped", fmt.Errorf("request failed: %w", raErr), requestedDelay}, + {"nested", fmt.Errorf("operation failed: %w", fmt.Errorf("request failed: %w", raErr)), requestedDelay}, + {"joined", errors.Join(unrelatedErr, raErr), requestedDelay}, + {"wrapped capped", fmt.Errorf("request failed: %w", cappedErr), maxDelay}, + {"joined capped", errors.Join(unrelatedErr, cappedErr), maxDelay}, + {"no retry after", fmt.Errorf("request failed: %w", unrelatedErr), fallbackDelay}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + clk := &testClock{afterChan: make(chan time.Time, 1)} + clk.afterChan <- time.Now() + var observedDelay time.Duration + calls := 0 + fallbackCalls := 0 + val, err := Do(context.Background(), func(context.Context) (string, error) { + calls++ + if calls == 1 { + return "", tt.err + } + return "ok", nil + }, WithAttempts(2), WithClock(clk), WithMaxDelay(maxDelay), + WithDelayFunc(func(int, error) time.Duration { + fallbackCalls++ + return fallbackDelay + }), + WithOnRetry(func(info RetryInfo) { observedDelay = info.Delay }), + ) + if err != nil || val != "ok" || calls != 2 { + t.Fatalf("Do returned (%q, %v) after %d calls", val, err, calls) + } + if observedDelay != tt.want { + t.Errorf("retry delay = %v, want %v", observedDelay, tt.want) + } + wantFallbackCalls := 0 + if tt.want == fallbackDelay { + wantFallbackCalls = 1 + } + if fallbackCalls != wantFallbackCalls { + t.Errorf("delay function called %d times, want %d", fallbackCalls, wantFallbackCalls) + } + }) + } +} + func TestDo_Generics(t *testing.T) { ctx := context.Background()