Flambe docs
Guides

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

StatusMeaningRetry?
400Malformed request, or validation failed.No — fix the request.
401No token, or it did not verify.Once, after refreshing. See below.
403Authenticated but not permitted.No.
404No such resource, or not visible to this user.No.
409The resource is not in a state that allows the operation.No — re-read state first.
429Rate limited.Yes, with backoff.
5xxServer-side failure.Yes, with backoff.
502An 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:

OperationCause
Retry an importIt is still in flight. Cancel it first.
Cancel an importIt already reached a terminal state.
Report an importIt has not completed, or this user already reported it.
Accept a household inviteThe 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-set and 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.

On this page