refactor(postgrest)!: fix the retried status codes to 503 and 520 - #1737
Conversation
Letting callers pass their own set invites retrying responses that cannot change, a 500 from a failing query for example, which only multiplies the load on the project. 503 and 520 are the only responses worth repeating, so PostgrestRetryOptions.statusCodes is a static constant now instead of a configurable field. Closes #1736
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (7)
💤 Files with no reviewable changes (1)
Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review. 📝 WalkthroughWalkthroughPostgREST retry configuration now uses ChangesPostgREST retry configuration
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to The PR narrows retries to 503 and 520 and updates the related tests and migration guidance; only routine validation remains, with no actionable merge-blocking risk identified. Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@grdsdev fix regarding what we spoke about yesterday :) |
Stacked on #1737, so review that one first. This diff is against that branch. ## What Retry was configured differently in every client: `postgrest` took a `PostgrestRetryOptions`, `supabase_storage` took an `int`, and the auth token refresh hardcoded its own numbers with no way to change them. All three take one `SupabaseRetryOptions` now. ```dart final supabase = SupabaseClient( supabaseUrl, supabaseKey, postgrestOptions: const PostgrestClientOptions( retryOptions: SupabaseRetryOptions(count: 5), ), storageOptions: const StorageClientOptions( retryOptions: SupabaseRetryOptions(count: 5), ), authOptions: const AuthClientOptions( retryOptions: SupabaseRetryOptions(count: 5), ), ); ``` The type carries `enabled`, `count`, `initialDelay`, `maxDelay` and `randomizationFactor`, plus `delay(attempt)`, `copyWith` and value equality. What counts as a retryable failure stays with each client, since those are not interchangeable: PostgREST repeats a read that answered with `503` or `520`, storage repeats an upload that hit a network error, and auth repeats a token refresh that never reached the service. The loops stay separate too, because the PostgREST one retries on a response status while the other two retry on a thrown exception. ### Where the type lives `supabase_common` is the only package all three clients depend on, so declaring the shared type in a client package would mean making storage depend on postgrest. It is declared there and re-exported from `postgrest`, `supabase_storage`, `supabase_auth`, `supabase` and `supabase_flutter`. `supabase_common` is excluded from the capability-matrix scan as a whole, which would have made the knobs unregistrable. `.sdk-parse-ignore` now excludes the package's sources with a negation for `retry_options.dart` alone, so the one genuinely user facing file of that package is scanned and the rest stays internal. ### One backoff curve Every client now backs off from 400 ms, doubling to 30 seconds, with 25% jitter. Only the count differs, and only where it has to: | Client | Before | After | | --- | --- | --- | | `postgrest` | 3 retries, 1s doubling to 30s, no jitter | 3 retries on the shared curve | | `supabase_storage` | opt-in, 400ms doubling to 30s, 25% jitter | unchanged, still opt-in with `count: 0` | | `supabase_auth` | 400ms doubling to 10s, no jitter, effectively unbounded | `count: 8`, still bounded by the refresh tick | PostgREST reads therefore back off sooner and with jitter, which is the one behavior change beyond the API. `MIGRATION.md` shows how to get the old curve back. The auth refresh predicate used to estimate the next backoff as `200 * 2^(attempt - 1)`, which was half the delay the runner actually waited. It asks the options for the real next delay now, so the tick deadline is checked against what will happen. ### Breaking changes | Before | After | | --- | --- | | `PostgrestRetryOptions` | `SupabaseRetryOptions` | | `PostgrestRetryOptions.statusCodes` | `PostgrestClient.retryableStatusCodes` | | `SupabaseStorageClient(retryAttempts: 5)` | `SupabaseStorageClient(retryOptions: SupabaseRetryOptions(count: 5))` | | `StorageClientOptions(retryAttempts: 5)` | `StorageClientOptions(retryOptions: …)` | | `upload(…, retryAttempts: 5)`, same on `uploadBinary`, `uploadToSignedUrl`, `uploadBinaryToSignedUrl`, `update` and `updateBinary` | `retryOptions: …` | | `RetryOptions` and `RetryOptions.retry` in `supabase_common` | the top-level `retry(action, options: …)` runner | `count` is the number of retries after the first attempt, which is how storage `retryAttempts` counted too, so those numbers carry over unchanged. `AuthClientOptions.retryOptions` and `AuthClient(retryOptions: …)` are new. ## Tests - `packages/supabase_common/test/retry_test.dart` is ported to the runner's new shape and covers `enabled`, a count of zero, the delay curve, `copyWith`, equality and both assertions. - New `packages/supabase_auth/test/refresh_retry_test.dart` proves the configured count and `enabled` bound the refresh attempts, which was not configurable before. - `packages/supabase_storage/test/fetch_test.dart` gains cases for the default of no retries, `enabled: false` and a per-upload override replacing the client options. - The `SupabaseRetryOptions` unit cases move out of `packages/postgrest/test/retry_test.dart` into `supabase_common`, and the delay test there pins `randomizationFactor: 0` now that jitter is on by default. - `sdk-compliance.yaml` registers the shared type under `database.configuration.auto_retry`, `StorageClientOptions.retryOptions` and `SupabaseStorageClient.retryOptions` under `storage.configuration.auto_retry`, and the auth knobs under `auth.session.auto_refresh`; symbol, drift and schema checks run clean locally. - `MIGRATION.md` and `AGENTS.md` are updated. Closes #1735 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added shared `SupabaseRetryOptions` for configuring retries across Auth, PostgREST, and Storage. * Added configurable retry behavior, including attempt limits, backoff delays, jitter, and enable/disable controls. * Added Auth refresh retry configuration and fixed PostgREST retryable status codes. * Added per-upload retry overrides for Storage. * **Documentation** * Updated migration and API documentation to describe the unified retry configuration and changed Storage defaults. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
What
PostgrestRetryOptions.statusCodesis a static constant now instead of a configurable field, so503 Service Unavailableand520 Unknown Errorare the only responses that ever trigger a retry.Letting callers pass their own set invites retrying responses that cannot change on a second attempt, a
500from a failing query for example, which only multiplies the load on the project without a chance of a different answer.Everything else about the retry configuration is unchanged:
enabled,count, the backoff knobs and the per-request.retry()override all stay.Since the set is a
constnow, the two places that snapshotted it withSet.unmodifiableso a caller could not mutate a live client's behavior are gone as well.Breaking changes
PostgrestRetryOptions(statusCodes: …)PostgrestRetryOptions.copyWith(statusCodes: …)PostgrestRetryOptions.statusCodes(instance field)PostgrestRetryOptions.statusCodes(static constant)PostgrestRetryOptions.defaultStatusCodesPostgrestRetryOptions.statusCodesThe v2
PostgrestClient(retryableStatusCodes:)andPostgrestClientOptions(retryableStatusCodes:)were already replaced byretryOptionsin #1731, and now have no replacement at all.MIGRATION.mdgets a section for that and the existing v3 retry section drops itsstatusCodesreferences.Tests
packages/postgrest/test/retry_test.dartloses the "configurable retryable status codes" group, gains an explicit "GET does not retry on 500" case and an assertion that the fixed set is{503, 520}.packages/supabase/test/postgrest_options_test.dartproved that the configured options were threaded throughfrom(),schema().from()andrpc()by retrying a custom status code. It now uses a retry count above the default of three instead, so the request only recovers on the fifth response.mainsince fix(gotrue)!: assert asyncStorage is provided for PKCE flow in the constructor #1489 landed, becauseSupabaseClientasserts on a PKCE flow withoutasyncStorage. It passes aMemoryAuthAsyncStoragenow, which is unrelated to the retry change but needed for the file to run at all.sdk-compliance.yamldropsPostgrestRetryOptions.defaultStatusCodes; symbol, drift and schema checks run clean locally.Closes #1736
Summary by CodeRabbit
PostgrestRetryOptionsvalue across clients and request builders.