feat(postgrest): add automatic retry for transient failures#1338
Conversation
Implements retry logic for the Flutter PostgREST client, mirroring the Swift SDK (SDK-771) and supabase-js reference implementations. Key behavior: - Only retries idempotent methods: GET and HEAD - Retry conditions: HTTP 520 or network/connection error - Up to 3 retries with exponential backoff: 1s → 2s → 4s (capped at 30s) - Adds X-Retry-Count: <n> header on each retry attempt - Enabled by default; disable globally via PostgrestClient(retryEnabled: false) - Per-request override via .retry(enabled: false/true) Acceptance Criteria: - [x] Retry logic only applies to GET and HEAD - [x] HTTP 520 and network errors trigger retries; other status codes do not - [x] Exponential backoff: 1s, 2s, 4s (capped at 30s) - [x] X-Retry-Count header present on retried requests - [x] retryEnabled: false on PostgrestClient disables globally - [x] .retry(enabled: false/true) overrides per request - [x] All existing tests pass - [x] 11 new tests cover all retry scenarios Linear: SDK-785 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds automatic retry behavior to the Dart/Flutter PostgREST client for transient failures, aligning behavior with other Supabase SDKs and improving resiliency for idempotent reads.
Changes:
- Introduces configurable automatic retries (default enabled) for GET/HEAD on HTTP 520 and network exceptions, including
X-Retry-Countheader and exponential backoff. - Propagates retry configuration through the various builder types and copy/transform flows.
- Adds a dedicated unit test suite covering retry scenarios with a mock
http.Client.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| packages/postgrest/lib/src/postgrest.dart | Adds global retryEnabled (default true) and test-only retryDelay, and forwards them into builders. |
| packages/postgrest/lib/src/postgrest_builder.dart | Implements retry loop, per-request override API (retry(enabled: ...)), and default exponential backoff. |
| packages/postgrest/lib/src/postgrest_query_builder.dart | Accepts/propagates client retry settings into underlying PostgrestBuilder. |
| packages/postgrest/lib/src/postgrest_rpc_builder.dart | Accepts/propagates client retry settings into underlying PostgrestBuilder. |
| packages/postgrest/lib/src/raw_postgrest_builder.dart | Ensures retry fields are preserved across copy/withConverter paths. |
| packages/postgrest/lib/src/response_postgrest_builder.dart | Ensures retry fields are preserved across response builder cloning/withConverter paths. |
| packages/postgrest/test/retry_test.dart | Adds unit tests validating retry eligibility, headers, exhaustion behavior, and per-request/global overrides. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Use initializing formal (this.retryEnabled) to fix prefer_initializing_formals - Remove @VisibleForTesting from retryDelay in PostgrestQueryBuilder and PostgrestRpcBuilder since they are called from production code in postgrest.dart; keep annotation only on PostgrestClient and PostgrestBuilder Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_execute() was writing Prefer, Accept-Profile, Content-Profile, Content-Type, and X-Retry-Count directly into _headers. Because _copyWith passes the same map reference when headers are not overridden, sibling builders share the map, and awaiting a builder more than once accumulates mutations. Switch to a per-execution local copy (execHeaders) so that _headers is never mutated, retry headers don't leak across requests, and repeated awaits behave correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
503 is a standard transient error (load shedding, rolling restarts) that should be retried the same way as 520. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
retry() on the base PostgrestBuilder returned PostgrestBuilder<T,S,R>, losing the concrete subclass type and breaking chains like from(...).select().retry(...).single() or from(...).retry(...).select().eq(...). Override retry() in PostgrestTransformBuilder, PostgrestFilterBuilder, and PostgrestQueryBuilder following the same pattern as setHeader(). Also add retryEnabled param to PostgrestQueryBuilder constructor so setHeader() and retry() both propagate the per-request override correctly. Additionally update the PostgrestClient doc comment to mention HTTP 503 as a retryable status code alongside 520. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Vinzent03
left a comment
There was a problem hiding this comment.
I'm not familiar with how it is done in the other libraries, but to reduce the number of arguments for the builder classes. What about removing the clientRetryEnabled in the builder and just setting retryEnabled on construction to the client value and making it non-nullable. I think this would make the behavior cleaner. The retryEnabled can than be changed by the .retry method.
In addition I would propose to make the retryDelay configurable in the .retry method, because is already stored in the builder so why not.
Main's SDK alignment commit (e37539e) removed ! assertions on nullable fields (_count, _schema, _isolate, _converter) expecting Dart field promotion to handle them. However, field promotion is blocked here because PostgrestBuilder has subclasses in the same library, preventing the analyzer from guaranteeing the field value hasn't changed. Restore ! assertions inside the null-checked contexts to fix dart analyze --fatal-warnings. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…-nullable Implements Vinzent's review suggestion: instead of carrying both _clientRetryEnabled and _retryEnabled (nullable) through every builder, set _retryEnabled to the client value on construction and keep it non-nullable. The .retry() method overrides it directly. Also removes unnecessary null-assertion operators (!) that Dart now correctly identifies as redundant via field promotion. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This reverts commit 33c4669.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Summary
Implements automatic retry logic for the Flutter PostgREST client, mirroring the Swift SDK (SDK-771) and the supabase-js reference implementation.
X-Retry-Count: <n>header on each retry attemptChanges
postgrest.dart: AddedretryEnabledparameter toPostgrestClient(defaulttrue); passes retry config into all builders created byfrom(),rpc(), andschema()postgrest_builder.dart: Added_clientRetryEnabled,_retryEnabled(per-request), and_retryDelayfields; addedretry({required bool enabled})method; extracted_executeWithRetry()wrapping the HTTP call with retry loop; addeddart:mathimport_execute()now works with a per-execution copy of headers (execHeaders) so that_headersis never mutated — fixes header leakage across repeated awaits and between sibling builders that share the same headers map referencepostgrest_query_builder.dart/postgrest_rpc_builder.dart: Accept and propagateclientRetryEnabled/retryDelayconstructor paramsraw_postgrest_builder.dart/response_postgrest_builder.dart: Copy retry fields in copy constructors andwithConverter()test/retry_test.dart(new): 12 unit tests using a mockhttp.Clientcovering all retry scenariosTesting
New Test Coverage (12 tests, all passing)
X-Retry-Countheader increments)SocketExceptionnetwork error.retry(enabled: false)disables retry per-requestPostgrestClient(retryEnabled: false)disables retry globally.retry(enabled: true)re-enables retry overriding client-levelfalseTests use zero-duration delay override (
retryDelay: (_) => Duration.zero) to run instantly.Existing Tests
All existing custom HTTP client tests continue to pass. Integration tests (requiring a live PostgREST server) are unaffected.
Risk Assessment
retryEnableddefaults totrue,retryDelayis@visibleForTestingAcceptance Criteria
X-Retry-Countheader present on retried requests, does not leak to non-retry requestsretryEnabled: falseonPostgrestClientdisables globally.retry(enabled: false/true)overrides per requestCloses: SDK-785
🤖 Generated with Claude Code
/take