Skip to content

feat(postgrest): add automatic retries for transient errors#2072

Open
grdsdev wants to merge 7 commits intomasterfrom
grdsdev/postgrest-auto-retry
Open

feat(postgrest): add automatic retries for transient errors#2072
grdsdev wants to merge 7 commits intomasterfrom
grdsdev/postgrest-auto-retry

Conversation

@grdsdev
Copy link
Copy Markdown
Contributor

@grdsdev grdsdev commented Jan 29, 2026

Summary

Implements automatic retry logic for @supabase/postgrest-js to handle transient errors transparently.

Problem

Two classes of transient errors cause unnecessary failures:

  1. 520 errors (Cloudflare timeout/connection errors under load) — customers perceive PostgREST as unreliable when these occur
  2. 503 + PGRST002 (PostgREST schema cache not yet loaded on a replica) — affects customers using load-balanced Supabase URLs, where a request may land on a replica that hasn't finished loading its schema cache. PostgREST signals this is safe to retry via a `Retry-After: 0` header

Solution

  • Automatic retries enabled by default for GET/HEAD/OPTIONS requests (idempotent only)
  • 3 retry attempts with exponential backoff (1s, 2s, 4s, max 30s)
  • Retries on 520 (Cloudflare transient errors) and 503 + Retry-After (PGRST002 schema cache)
  • Response body drained before retrying to avoid stream leaks
  • AbortError rethrown immediately — aborted requests are never retried
  • Simple boolean API to enable/disable retries globally or per-request
  • Network errors retried for idempotent methods (with same backoff)

Note: The X-Retry-Count diagnostic header was intentionally omitted from this PR to avoid CORS preflight issues with static Access-Control-Allow-Headers configs. It will be introduced in a future PR alongside an update to cors.ts.

Usage

// Retries enabled by default - no configuration needed
const { data, error } = await supabase.from('users').select()

// Disable retries globally
const client = new PostgrestClient('https://api.example.com', {
  retry: false,
})

// Disable retries per-request
const { data, error } = await supabase
  .from('users')
  .select()
  .retry(false)

// Re-enable per-request when globally disabled
const { data, error } = await supabase
  .from('users')
  .select()
  .retry(true)

Key Design Decisions

Decision Rationale
Enabled by default Gives perception of more reliable service without opt-in friction
Only GET/HEAD/OPTIONS Non-idempotent methods (POST/PATCH/DELETE) could cause data duplication
520 + 503/PGRST002 520 = Cloudflare transient; 503 = PostgREST schema cache loading (signals retry via Retry-After)
Respects Retry-After header PostgREST always sends Retry-After: 0 for PGRST002 — no unnecessary delay
AbortError not retried User explicitly cancelled; retrying would violate intent
Body drained before retry Avoids stream leaks on retried responses
No X-Retry-Count header Deferred to avoid CORS breakage for users with static allowlists

Test plan

  • Retry on 520 errors (GET/HEAD), no retry on POST
  • Retry on 503/PGRST002 with Retry-After header, surfaces error after max retries
  • Response body drained before retry
  • AbortError rethrown immediately, no retry
  • Network errors retried for GET, not for POST
  • Per-request .retry(false) overrides global default
  • Per-request .retry(true) overrides global retry: false
  • Exponential backoff delays verified
  • Schema switching preserves retry setting
  • All existing integration tests pass

🤖 Generated with Claude Code

@github-actions github-actions bot added the postgrest-js Related to the postgrest-js library. label Jan 29, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 29, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • ✅ Full review completed - (🔄 Check again to review again)

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/core/postgrest-js/src/PostgrestBuilder.ts`:
- Around line 193-244: The retry loop in executeWithRetry currently retries on
520 responses without consuming the response body and also may retry on
AbortError; update executeWithRetry so that when shouldRetry(...) returns true
you first fully drain the response body (e.g., await res.arrayBuffer() or
res.text() to consume streams) before sleeping/retrying, and in the catch block
immediately rethrow if fetchError.name === 'AbortError' (do not treat as
retryable), otherwise keep existing logic using RETRYABLE_METHODS,
DEFAULT_MAX_RETRIES, getRetryDelay, sleep, and continue calling _fetch and
processResponse as before.
🧹 Nitpick comments (3)
packages/core/postgrest-js/test/retry.test.ts (1)

210-235: Consider a more robust approach for verifying backoff delays.

The current implementation spies on setTimeout and immediately executes callbacks with delay=0. While functional, this approach bypasses the actual timing behavior and could mask issues if the retry implementation changes how it schedules delays.

A more robust alternative would use vi.getTimerCount() or advance timers by specific amounts and verify fetch call counts at each interval.

packages/core/postgrest-js/src/PostgrestQueryBuilder.ts (2)

24-25: Add JSDoc documentation for the retry property.

The retry property is part of the public API but lacks JSDoc documentation. Other public properties in this class could benefit from similar documentation, but at minimum the new retry property should be documented to explain its purpose and default behavior.

📝 Suggested documentation
+  /**
+   * Enable or disable automatic retries for transient errors.
+   * When enabled, idempotent requests (GET/HEAD/OPTIONS) that fail with 520 errors
+   * will be automatically retried with exponential backoff.
+   */
   // Retry configuration
   retry?: boolean

As per coding guidelines: "Use JSDoc comments for all public APIs in each library".


40-59: Add retry parameter to the constructor's JSDoc.

The constructor example and parameter documentation should include the new retry option for completeness.

📝 Suggested documentation update
   /**
    * Creates a query builder scoped to a Postgres table or view.
    *
    * `@example`
    * ```ts
    * import PostgrestQueryBuilder from '@supabase/postgrest-js'
    *
    * const query = new PostgrestQueryBuilder(
    *   new URL('https://xyzcompany.supabase.co/rest/v1/users'),
-   *   { headers: { apikey: 'public-anon-key' } }
+   *   { headers: { apikey: 'public-anon-key' }, retry: true }
    * )
    * ```
+   *
+   * `@param` url - The URL for the query
+   * `@param` options - Named parameters
+   * `@param` options.headers - Custom headers
+   * `@param` options.schema - Postgres schema
+   * `@param` options.fetch - Custom fetch function
+   * `@param` options.retry - Enable automatic retries for transient errors
    */

Implement automatic retry logic for PostgREST SDK requests that fail
with 520 status codes (Cloudflare timeout/connection errors).

Key features:
- Enabled by default for GET/HEAD/OPTIONS requests
- 3 retry attempts with exponential backoff (1s, 2s, 4s)
- X-Retry-Count header sent for observability
- Can be disabled globally or per-request via .retry(false)

This addresses customer feedback about perceived reliability issues
when transient 520 errors occur under load.

Related: https://linear.app/supabase/project/automatic-retries-for-postgrest-sdk-c6fa4eaf163a

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@mandarini mandarini force-pushed the grdsdev/postgrest-auto-retry branch from 3734cba to 8c9359d Compare March 27, 2026 13:57
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 27, 2026

Open in StackBlitz

@supabase/auth-js

npm i https://pkg.pr.new/@supabase/auth-js@2072

@supabase/functions-js

npm i https://pkg.pr.new/@supabase/functions-js@2072

@supabase/postgrest-js

npm i https://pkg.pr.new/@supabase/postgrest-js@2072

@supabase/realtime-js

npm i https://pkg.pr.new/@supabase/realtime-js@2072

@supabase/storage-js

npm i https://pkg.pr.new/@supabase/storage-js@2072

@supabase/supabase-js

npm i https://pkg.pr.new/@supabase/supabase-js@2072

commit: 30c0fc8

@mandarini mandarini force-pushed the grdsdev/postgrest-auto-retry branch from 8c9359d to 63fd316 Compare March 27, 2026 13:59
@mandarini mandarini force-pushed the grdsdev/postgrest-auto-retry branch from 274617c to 7f6bc9b Compare March 27, 2026 14:32
@mandarini mandarini self-assigned this Mar 27, 2026
@mandarini mandarini marked this pull request as ready for review March 27, 2026 14:42
@mandarini mandarini requested review from a team as code owners March 27, 2026 14:42
Copilot AI review requested due to automatic review settings March 27, 2026 14:44
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@mandarini mandarini self-requested a review March 27, 2026 14:48
@mandarini mandarini requested a review from steve-chavez March 27, 2026 15:36
@steve-chavez
Copy link
Copy Markdown
Member

Note: The X-Retry-Count diagnostic header was intentionally omitted from this PR to avoid CORS preflight issues with static Access-Control-Allow-Headers configs. It will be introduced in a future PR alongside an update to cors.ts.

The lack of retry header is concerning, without a retry header it will be too hard to debug at the origin server. Can that be added in this PR?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

postgrest-js Related to the postgrest-js library.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants