Errors
Retry rules and backoff behaviour for idempotent calls.
Blindly resending a failed call can create a reservation twice or dig a rate limit deeper. Retrying comes down to three questions: is this call idempotent, how long should you wait before the next attempt, and is this error the kind a retry can fix. The envelope from the error model answers the last one directly.
GET and DELETE are idempotent. Sending the same request again does not change the outcome, so after a network error or a 502 you can retry them freely. POST is not idempotent by default: resending a Create reservation call that never got an answer can book the table twice.
| Method | Idempotent | Retry |
|---|---|---|
| GET | Yes | Safe. Retry with backoff and jitter. |
| DELETE | Yes | Safe. Repeating the same cancellation does not change the outcome. |
| POST | No | State creating POSTs are safe only with an Idempotency-Key header. Read only POSTs such as Estimate ride say so in their reference description and are safe as is. |
To make a POST retryable, send an Idempotency-Key header on the first attempt. The gateway remembers the key for 24 hours and answers any repeat of it with the result of the first processing. Every state creating POST accepts the header, including Create reservation and Join waitlist.
curl -X POST "https://api.frism.dev/v1/catch-table/reservations" \
-H "Authorization: Bearer frism_test_k3xample" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: idem_b1f7c2d0" \
-d '{
"restaurant_id": "res_01J8Q9W3TE",
"slot_id": "slt_1900_2",
"party_size": 2,
"contact_name": "Han Jiwoo"
}'Grow the wait between attempts exponentially and add random jitter to each one. When every client retries on a fixed schedule, a service recovering from an outage gets hit by all of them at once, which slows the recovery down. There is one exception: a 429 carries a Retry-After header, and when it is present you follow the header instead of your computed backoff.
Cap the number of attempts. A request that has not succeeded after three or four retries should return its error rather than sit in a queue, letting the caller inform the user or try again later. A client that retries without a ceiling only prolongs the outage.
The retryable field in the envelope is the gateway's own verdict on whether a retry can succeed. It is true on 429 rate_limited and 502 provider_unavailable. Branching on this flag instead of hardcoding a status list means your client keeps working if the mapping ever grows.
The helper below implements all of the rules above. It only retries network errors, 429 and 502, prefers Retry-After when the response carries one, and falls back to full jitter backoff otherwise.
const BASE_URL = "https://api.frism.dev/v1";
const RETRYABLE_STATUSES = new Set([429, 502]);
interface FrismRequest {
method?: "GET" | "POST" | "DELETE";
headers?: Record<string, string>;
body?: string;
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Full jitter: a random wait between 0 and the exponential ceiling.
function backoffMs(attempt: number, baseMs = 500, capMs = 8_000): number {
const ceiling = Math.min(capMs, baseMs * 2 ** (attempt - 1));
return Math.random() * ceiling;
}
export async frismFetch(
path: string,
init: FrismRequest = {},
maxAttempts = ,
): Promise<Response> {
for ( attempt = ; ; attempt += ) {
response: Response | undefined;
{
response = fetch(`${BASE_URL}${path}`, {
...init,
headers: {
Authorization: ,
...init.headers,
},
});
} (cause) {
(attempt >= maxAttempts) cause;
}
(response) {
(!RETRYABLE_STATUSES.has(response.status)) response;
(attempt >= maxAttempts) response;
}
retryAfter = Number(response?.headers.get());
delayMs = retryAfter > ? retryAfter * : backoffMs(attempt);
sleep(delayMs);
}
}When a POST goes through it, pass the Idempotency-Key along.
const response = await frismFetch("/catch-table/reservations", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": "idem_b1f7c2d0",
},
body: JSON.stringify({
restaurant_id: "res_01J8Q9W3TE",
slot_id: "slt_1900_2",
party_size: 2,
contact_name: "Han Jiwoo",
}),
});
if (!response.ok) {
// Reaching here means no retry can fix it. Branch on the code.
const { error } = (await response.json()) as { error: { code: string } };
console.error(error.code);
}conflict: the resource moved on. Do not repeat the same request; re-read and build a new request with another slot.provider_rejected: the service's policy refused the request. Sending it again returns the same answer every time.What each status and code means is laid out in the . And the fewer things you poll, the fewer retries you need: state changes arrive as pushes, see the .