Errors and retries¶
The error envelope¶
Every 4xx and 5xx response under /api/v1/ has the same shape:
{
"error": {
"code": "invalid",
"message": "Request validation failed.",
"details": {
"analysis_type": ["\"nope\" is not a valid choice."],
"image_id": ["This field is required."]
}
}
}
code- A short, stable, machine-readable string. Branch on this and the HTTP status.
message- A sentence for people. We improve these over time, so don't match on them.
details- For a validation error (
400), an object mapping each field to a list of problems. Parameter errors appear under the parameter's own name, such asdetails.max_iterations, and a problem with the parameters as a whole underdetails.__all__. Problems that aren't about one field appear undersubmission,upload,joborparameters. For other errors,detailsis{"detail": "<message>"}.
Messages never name the services or machines that run your work, and never carry a stack trace. A job or image that fails reports the solver's own explanation when it gives one, or a plain sentence such as "The analysis failed on the compute worker."
Status codes¶
| Status | error.code |
When | What to do |
|---|---|---|---|
400 |
invalid |
A field or parameter is missing or out of range, the image isn't ready or can't be analysed, the result isn't available yet, or no compute tier can fit the image. |
Fix the request. Retrying it unchanged gives the same answer. |
400 |
parse_error |
The body isn't valid JSON. | Fix the body. |
401 |
not_authenticated, authentication_failed |
Missing or bad credentials. | See Authentication. |
403 |
permission_denied |
The key's owner is in this team but the key isn't valid for it, or result access is paused (see below). | Use a key made by an owner of this team. See Authentication. |
403 |
analysis_unavailable |
The analysis isn't released to you on this team. | Check GET /analysis-types/. |
404 |
not_found |
The team, image or job doesn't exist, belongs to another team, or is in a team you can't reach. | Check the ID and the key. |
405 |
method_not_allowed |
Wrong HTTP method for the path. | — |
409 |
idempotency_conflict |
The Idempotency-Key was already used for a different submission. |
Use a new key for a new job. See Idempotency. |
415 |
unsupported_media_type |
The body's Content-Type isn't one the API reads. |
Send Content-Type: application/json. |
429 |
throttled |
Over the rate limit. | Wait Retry-After seconds. |
5xx |
— | Something went wrong on our side. | Retry with backoff. |
Retrying safely¶
- Reads (
GET) are always safe to retry. - Job submission isn't safe to repeat blindly. Always send an
Idempotency-Key. If a request times out, resend it with the same key: you get the original job back (200withIdempotency-Replayed: true). Without a key, a blind retry can create a second job. - Upload sessions expire after one hour. If the presigned upload is rejected as expired, create a new session. Don't retry the old form. Once the file is uploaded, complete the upload within 6 days: a later completion is refused, and the file can be deleted any time after 7 days.
- Back off. Start around 1 s, double each time, add random jitter, and give up after a few minutes.
import random
import time
def call(method, url, retries=6, **kwargs):
"""Send a request, retrying 429s and 5xx responses with exponential backoff."""
for attempt in range(retries):
response = session.request(method, url, **kwargs)
if response.status_code == 429:
time.sleep(float(response.headers.get("Retry-After", 2 ** attempt)))
continue
if response.status_code >= 500:
time.sleep(2 ** attempt + random.random())
continue
return response
response.raise_for_status()
return response