Skip to content

refactor(postgrest)!: fix the retried status codes to 503 and 520 - #1737

Merged
spydon merged 1 commit into
mainfrom
refactor/postgrest-fixed-retry-status-codes
Aug 19, 2026
Merged

refactor(postgrest)!: fix the retried status codes to 503 and 520#1737
spydon merged 1 commit into
mainfrom
refactor/postgrest-fixed-retry-status-codes

Conversation

@spydon

@spydon spydon commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

What

PostgrestRetryOptions.statusCodes is a static constant now instead of a configurable field, so 503 Service Unavailable and 520 Unknown Error are 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 500 from a failing query for example, which only multiplies the load on the project without a chance of a different answer.

// Before
retryOptions: PostgrestRetryOptions(count: 5, statusCodes: {500, 503, 520}),

// After
retryOptions: PostgrestRetryOptions(count: 5),

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 const now, the two places that snapshotted it with Set.unmodifiable so a caller could not mutate a live client's behavior are gone as well.

Breaking changes

Before After
PostgrestRetryOptions(statusCodes: …) removed
PostgrestRetryOptions.copyWith(statusCodes: …) removed
PostgrestRetryOptions.statusCodes (instance field) PostgrestRetryOptions.statusCodes (static constant)
PostgrestRetryOptions.defaultStatusCodes PostgrestRetryOptions.statusCodes

The v2 PostgrestClient(retryableStatusCodes:) and PostgrestClientOptions(retryableStatusCodes:) were already replaced by retryOptions in #1731, and now have no replacement at all. MIGRATION.md gets a section for that and the existing v3 retry section drops its statusCodes references.

Tests

  • packages/postgrest/test/retry_test.dart loses 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.dart proved that the configured options were threaded through from(), schema().from() and rpc() 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.
  • That file also fails on main since fix(gotrue)!: assert asyncStorage is provided for PKCE flow in the constructor #1489 landed, because SupabaseClient asserts on a PKCE flow without asyncStorage. It passes a MemoryAuthAsyncStorage now, which is unrelated to the retry change but needed for the file to run at all.
  • sdk-compliance.yaml drops PostgrestRetryOptions.defaultStatusCodes; symbol, drift and schema checks run clean locally.

Closes #1736

Summary by CodeRabbit

  • Breaking Changes
    • Retry configuration now uses a single PostgrestRetryOptions value across clients and request builders.
    • Automatic retries are limited to HTTP 503 and 520 responses.
    • Custom retry status codes are no longer supported; handle other statuses by catching API exceptions.
    • Per-request retry count overrides remain available.
  • Tests
    • Updated coverage confirms retry counts and status-code behavior.

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
@spydon
spydon requested a review from a team as a code owner August 19, 2026 07:41
@coderabbitai

coderabbitai Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 82334ea4-9e18-4979-ad6f-d9a0041bb909

📥 Commits

Reviewing files that changed from the base of the PR and between 30729cc and 3eeecc6.

📒 Files selected for processing (7)
  • MIGRATION.md
  • packages/postgrest/lib/src/postgrest.dart
  • packages/postgrest/lib/src/postgrest_builder.dart
  • packages/postgrest/lib/src/postgrest_retry_options.dart
  • packages/postgrest/test/retry_test.dart
  • packages/supabase/test/postgrest_options_test.dart
  • sdk-compliance.yaml
💤 Files with no reviewable changes (1)
  • sdk-compliance.yaml

Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

PostgREST retry configuration now uses PostgrestRetryOptions across clients and builders. Retry status codes are fixed to 503 and 520. Tests and migration guidance remove custom status-code configuration and retain per-request retry overrides.

Changes

PostgREST retry configuration

Layer / File(s) Summary
Fixed retry options and migration guidance
packages/postgrest/lib/src/postgrest_retry_options.dart, MIGRATION.md
PostgrestRetryOptions now exposes fixed status codes 503 and 520. Constructor and copyWith status-code parameters were removed.
Client and builder retry execution
packages/postgrest/lib/src/postgrest.dart, packages/postgrest/lib/src/postgrest_builder.dart
Clients and builders retain the supplied retry options. Retry checks use the fixed PostgrestRetryOptions.statusCodes set.
Retry behavior validation and compliance metadata
packages/postgrest/test/retry_test.dart, packages/supabase/test/postgrest_options_test.dart, sdk-compliance.yaml
Tests verify fixed status handling and custom retry counts. The capability matrix removes the deleted defaultStatusCodes symbol.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 3eeec

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: v3

Suggested reviewers: tr00d, vinzent03

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: retries are restricted to HTTP 503 and 520.
Linked Issues check ✅ Passed The changes satisfy issue #1736 by fixing retries to 503 and 520 and removing configurable status-code sets.
Out of Scope Changes check ✅ Passed The code, tests, migration documentation, and compliance updates directly support the retry status-code change.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/postgrest-fixed-retry-status-codes

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@spydon

spydon commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

@grdsdev fix regarding what we spoke about yesterday :)

@spydon
spydon merged commit 17487ff into main Aug 19, 2026
39 checks passed
@spydon
spydon deleted the refactor/postgrest-fixed-retry-status-codes branch August 19, 2026 12:35
spydon added a commit that referenced this pull request Aug 19, 2026
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 -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

503 and 520 are the only status codes that should be retried

2 participants