File size: 4,253 Bytes
96c8713 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | import {
DEFAULT_MAX_RETRIES,
Fetch,
getRetryDelay,
RETRYABLE_METHODS,
RETRYABLE_STATUS_CODES,
} from './types/common/common'
/**
* Sleep for a given number of milliseconds.
* If an AbortSignal is provided, the sleep resolves early when the signal is aborted.
*/
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
return new Promise((resolve) => {
if (signal?.aborted) {
resolve()
return
}
const id = setTimeout(() => {
signal?.removeEventListener('abort', onAbort)
resolve()
}, ms)
function onAbort() {
clearTimeout(id)
resolve()
}
signal?.addEventListener('abort', onAbort)
})
}
/**
* Check if a request should be retried based on method and status code.
*/
function shouldRetry(
method: string,
status: number,
attemptCount: number,
retryEnabled: boolean
): boolean {
// Don't retry if retries are disabled or we've exhausted attempts
if (!retryEnabled || attemptCount >= DEFAULT_MAX_RETRIES) {
return false
}
// Only retry idempotent methods (GET, HEAD, OPTIONS)
if (!RETRYABLE_METHODS.includes(method as (typeof RETRYABLE_METHODS)[number])) {
return false
}
// Only retry on specific status codes (520 - Cloudflare errors)
if (!RETRYABLE_STATUS_CODES.includes(status as (typeof RETRYABLE_STATUS_CODES)[number])) {
return false
}
return true
}
export interface RetryableRequest {
method: string
/** Plain object, never a Headers instance: React Native's XHR-based fetch drops the latter. */
headers: Record<string, string>
body?: string
signal?: AbortSignal
}
/**
* Perform a request with the retry policy shared by every PostgREST call.
*
* Idempotent methods (GET, HEAD, OPTIONS) are retried up to
* `DEFAULT_MAX_RETRIES` times when the fetch rejects or the server answers
* with a retryable status (503, 520). The wait honours the `Retry-After`
* header when present and backs off exponentially otherwise. Retried
* attempts carry an `X-Retry-Count` header. Aborted requests and
* non-idempotent methods are never retried: their rejection propagates
* unchanged. A response body is drained before its request is retried.
*/
export async function fetchWithRetry(
fetchImpl: Fetch,
url: string,
request: RetryableRequest,
retryEnabled: boolean
): Promise<Response> {
let attemptCount = 0
while (true) {
const headers: Record<string, string> = { ...request.headers }
if (attemptCount > 0) {
headers['X-Retry-Count'] = String(attemptCount)
}
let res: Response
try {
res = await fetchImpl(url, {
method: request.method,
headers,
body: request.body,
signal: request.signal,
})
// JS allows throwing any value, and serverless or realm-crossing fetch
// implementations can reject with non-Error objects. `instanceof Error`
// is too narrow here; narrow at the use site with optional chaining.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (fetchError: any) {
// Never retry aborted requests
if (fetchError?.name === 'AbortError' || fetchError?.code === 'ABORT_ERR') {
throw fetchError
}
// Don't retry network errors for non-idempotent methods
if (!RETRYABLE_METHODS.includes(request.method as (typeof RETRYABLE_METHODS)[number])) {
throw fetchError
}
// Check if we should retry network errors
if (retryEnabled && attemptCount < DEFAULT_MAX_RETRIES) {
const delay = getRetryDelay(attemptCount)
attemptCount++
await sleep(delay, request.signal)
continue
}
// Exhausted retries or retries disabled, throw the last error
throw fetchError
}
// Check if we should retry this HTTP response
if (shouldRetry(request.method, res.status, attemptCount, retryEnabled)) {
const retryAfterHeader = res.headers?.get('Retry-After') ?? null
const delay =
retryAfterHeader !== null
? Math.max(0, parseInt(retryAfterHeader, 10) || 0) * 1000
: getRetryDelay(attemptCount)
await res.text()
attemptCount++
await sleep(delay, request.signal)
continue
}
return res
}
}
|