Every error has the same JSON shape. Branch on error.code, show error.message, and log error.request_id.
Read the envelope
Section titled “Read the envelope”{ "error": { "code": "SKU_NOT_IN_SEARCH", "message": "This product was not a result of that search. Use the search_session_uuid of the search (or the page) that returned it.", "field": "sku_uuid", "request_id": "e19b8889-a63b-4000-9dbe-ffd5b9157bd5" }}| Field | Use |
|---|---|
code | Stable. Write your logic against it |
message | Written for a person. It can change. Never match on it |
field | The request field at fault, as a dotted path, or null |
request_id | Quote it to Flume support. Also in the X-Request-Id response header |
details | Sometimes present, with more about a refusal |
Decide what to retry
Section titled “Decide what to retry”What to do when a server call fails. Read the status and error.code on the left, then take the one action beside it.
Read this diagram as text
- 401 PARTNER_KEY_INVALID: fix the key, or the base URL it belongs to. Do not retry.
- 401 USER_TOKEN_INVALID: delete the stored token and start the session again without it. The user verifies again in the iframe.
- 401 CONNECT_CODE_INVALID: store nothing and do not retry. The user verifies again on the next open.
- 403 USER_NOT_CONNECTED: the call needs a verified user. Verify the user, then send the user access token.
- 400, 404, 413, 422, and any other 403 or 409: fix the request. On a 422, field names what is wrong. Do not retry unchanged.
- 409 IDEMPOTENCY_IN_PROGRESS: the first request with this Idempotency-Key is still running. Retry with the same key after Retry-After.
- 429: wait for the number of seconds in Retry-After, then retry. For a daily limit that is the time until 00:00 UTC, so stop for the day.
- 500, 502, 503 or a timeout: retry with a growing wait and the same Idempotency-Key, up to 3 retries.
| Status | Retry? | How |
|---|---|---|
400, 404, 413, 422 | No | Fix the request |
401 | No | Fix the key, or verify the user again |
403 | No | Send the user access token, or verify the user |
409 IDEMPOTENCY_IN_PROGRESS | Yes | After Retry-After seconds (2 when the header is absent), same key |
409 other codes | No | Resolve the conflict |
429 | Yes | After Retry-After seconds |
500 INTERNAL | Yes | With backoff. Same Idempotency-Key |
502 VERIFICATION_EMAIL_FAILED | Yes | With backoff |
503 SEARCH_UNAVAILABLE | Yes | With backoff |
Use exponential backoff with jitter. Double the wait each time, and stop after 4 tries.
Example: a retry wrapper
Section titled “Example: a retry wrapper”import randomimport time
import requests
RETRYABLE = {"IDEMPOTENCY_IN_PROGRESS", "RATE_LIMITED", "INTERNAL", "VERIFICATION_EMAIL_FAILED", "SEARCH_UNAVAILABLE"}
def call(method, url, **kwargs): for attempt in range(4): response = requests.request(method, url, timeout=75, **kwargs) if response.ok: return response try: error = response.json()["error"] except (ValueError, KeyError): response.raise_for_status() if error["code"] not in RETRYABLE or attempt == 3: raise RuntimeError(f'{error["code"]} {error["request_id"]}: {error["message"]}') wait = float(response.headers.get("Retry-After", 2**attempt)) if wait > 60: raise RuntimeError(f'{error["code"]}: retry in {wait:.0f} seconds') time.sleep(wait + random.random())const RETRYABLE = new Set(["IDEMPOTENCY_IN_PROGRESS", "RATE_LIMITED", "INTERNAL", "VERIFICATION_EMAIL_FAILED", "SEARCH_UNAVAILABLE"]);
export async function call(url, init) { for (let attempt = 0; attempt < 4; attempt += 1) { const response = await fetch(url, init); if (response.ok) return response; const body = await response.json().catch(() => null); const error = body?.error; if (!error) throw new Error(`HTTP ${response.status}`); if (!RETRYABLE.has(error.code) || attempt === 3) { throw new Error(`${error.code} ${error.request_id}: ${error.message}`); } const wait = Number(response.headers.get("Retry-After") ?? 2 ** attempt); if (wait > 60) throw new Error(`${error.code}: retry in ${wait} seconds`); await new Promise((resolve) => setTimeout(resolve, (wait + Math.random()) * 1000)); }}Keep the same Idempotency-Key in init.headers across the retries of one action.
To retry an upload such as POST /search, build the body again for each attempt. In Python, open the file again or call image.seek(0). A file that was already read uploads as empty, and the call returns 400 IMAGE_REQUIRED.
Handle responses that are not the envelope
Section titled “Handle responses that are not the envelope”- On live, a request from an unsupported country gets a
403with no JSON body. See Environments. - On live, a network-level limit per IP address can return a
429with no JSON body and noRetry-After. - A network timeout has no body. Retry a write with the same
Idempotency-Key.
Always check that the body parses as JSON and has error before you read it.
Show errors to users
Section titled “Show errors to users”error.messageis written for the user. You can show it.- For
429 RATE_LIMITED, tell the user when to try again.
Log for support
Section titled “Log for support”Log these for every failed call:
X-Request-Id- the method and path
error.code- the key prefix, never the key
session_uuid, for iframe sessions
See also
Section titled “See also”- Errors: every code, with its status and fix
- Idempotency
- Limits