Errors and retries
The error envelope, what each status means, and what is safe to retry.
The envelope
Errors return a non-2xx status and one of two shapes.
A general failure:
{ "error": "Recipe not found" }A request-validation failure, listing the offending fields:
{
"errors": [
{ "type": "field", "path": "title", "msg": "Invalid value", "location": "body" }
]
}Both can come back with 400, so handle either:
type FlambeError = { error: string } | { errors: Array<{ path: string; msg: string }> };
function describe(body: FlambeError): string {
if ('errors' in body) {
return body.errors.map((e) => `${e.path}: ${e.msg}`).join(', ');
}
return body.error;
}error is a human-readable message, not a stable machine identifier. Branch on
the status code, and on the message only where this documentation calls
out a specific string. Messages can be reworded.
Status codes
| Status | Meaning | Retry? |
|---|---|---|
400 | Malformed request, or validation failed. | No — fix the request. |
401 | No token, or it did not verify. | Once, after refreshing. See below. |
403 | Authenticated but not permitted. | No. |
404 | No such resource, or not visible to this user. | No. |
409 | The resource is not in a state that allows the operation. | No — re-read state first. |
429 | Rate limited. | Yes, with backoff. |
5xx | Server-side failure. | Yes, with backoff. |
502 | An upstream provider failed. | Yes, sparingly. |
401
Branch on the message, because the two cases want opposite handling:
Access token required— the header was never sent. A client bug. Refreshing cannot help, and retrying produces an infinite loop.Invalid or expired token— refresh the session and retry once.
404 vs 403
A resource you cannot see returns 404, not 403. This is deliberate: 403
would confirm the resource exists. Do not infer from a 404 that an id is
invalid.
409
409 means state, not syntax. The common cases:
| Operation | Cause |
|---|---|
| Retry an import | It is still in flight. Cancel it first. |
| Cancel an import | It already reached a terminal state. |
| Report an import | It has not completed, or this user already reported it. |
| Accept a household invite | The caller is already a member. |
Re-read the resource before deciding what to do — a 409 usually means your
view of its state is stale.
Retrying well
Retry 429 and 5xx. Never retry 4xx other than a single post-refresh
attempt on 401.
async function withRetry<T>(fn: () => Promise<Response>, parse: (r: Response) => Promise<T>) {
const maxAttempts = 4;
for (let attempt = 1; ; attempt++) {
const res = await fn();
if (res.ok) return parse(res);
const retryable = res.status === 429 || res.status >= 500;
if (!retryable || attempt === maxAttempts) {
throw new Error(`${res.status}: ${describe(await res.json())}`);
}
// Exponential backoff with jitter, so a fleet of clients does not
// synchronize its retries into a second thundering herd.
const base = 500 * 2 ** (attempt - 1);
await new Promise((r) => setTimeout(r, base + Math.random() * base));
}
}Two rules worth stating plainly:
- Add jitter. Without it, every client that failed together retries together.
- Cap attempts. An unbounded retry against a degraded service is indistinguishable from an attack on it.
Idempotency
Most write endpoints are not idempotent — retrying POST /api/imports after a
timeout creates a second import. Where retry safety matters:
- Timers accept
idempotencyKey. A repeat with the same key returns the original timer instead of creating another. - Imports have no idempotency key. If a create times out, list
GET /api/imports/working-setand look for the job before creating another. - Push tokens are keyed on the token itself, so re-registering updates rather than duplicating.
Rate limits
Limits are applied per IP. Exceeding them returns 429. Back off as above; do
not spread the same workload across addresses to evade the limit.
When it is not you
If calls are failing broadly, check status.flambe.dev before debugging your client. It publishes live component health — including each import queue separately — and you can subscribe for email notice of incidents.