Skip to main content
.md

Errors

Authoritative source

The per-code list and the typed error.details shapes are in the published OpenAPI schema — browse them in ReDoc or fetch /v1/openapi.json. This page covers the envelope, the discriminate-on-code rule, the handling strategy, and a few codes whose behavior the schema can't fully express.

Every non-2xx response uses the unified envelope:

{
"error": {
"code": "<machine_readable_snake_case>",
"message": "<human-readable summary>",
"request_id": "<server-assigned id>",
"details": "<optional, code-specific>"
}
}

error.code is the authoritative discriminator. Never parse error.message. The same request_id appears on the X-Request-Id response header and on every server log line for the request — quote it in support tickets.

Read request_id from the header if the body omits it

The X-Request-Id header is set on every response, including any error whose body omits error.request_id. Read error.request_id or X-Request-Id so you always have a correlation id to quote. The bundled client does this for you.

The code list

code is a closed enum in the schema — see the ErrorBody.code definition in ReDoc or /v1/openapi.json for the current set.

A new code can ship before your client is regenerated — so treat an unlisted value as a generic failure, not a parse error. (The schema says so explicitly; a generated client that rejects unknown enum members will break on the next release, not on a bug.)

Reading error.details

details is an anyOf over ValidationFailedDetails | TaskNotSupportedByModelDetails | ModelNotFoundDetails | SyncTooLargeDetails | GenericDetails | null, keyed on the sibling code. Switch on code first, then read details; codes without a typed shape carry only the human-readable message and should be treated as opaque.

Read details defensively — and never branch on loc

One trap, still live — plus one shape note:

  1. Shape. For validation_failed, details is usually {"errors": [...]}, matching the schema; each entry has the standard FastAPI {loc, msg, type} shape. Older releases emitted the bare per-field array, so defensive clients that accept either stay correct. One validation_failed breaks that shape: the splice response-size cap sends {task, record_count, maximum_records, sequence_length, threshold} and no errors key (see below). Read details.get("errors", []) rather than indexing it.
  2. loc. Some checks report where you'd expect — a sequence bound at ["body","sequence"], an unknown option key at ["body","options","<key>"] — but expression's tss_index checks come from a whole-model validator and report at ["body"]. There is no ["body","tss_index"] to match on, and matching on it will silently never fire. Match on error.code; use message for display only.

Notable codes

Most codes are self-explanatory from the OpenAPI schema. A few have behavior worth noting:

  • 504 (no envelope): a sync request past the upstream read timeout is terminated by the edge proxy with the proxy's body, not the unified {error: {...}} envelope (see Limits). Retry with Prefer: respond-async.
  • 404 not_found on an unknown task: an unrecognised {task} segment — on either POST /v1/tasks/{task}/predict or GET /v1/tasks/{task}/models — is a 404, not a 422. Only the six published tasks exist.
  • 404 not_found on cross-account access: accessing another caller's job returns 404, not 403, to prevent id enumeration.
  • The two 413s are different failures. payload_too_large is the 16 MiB raw-body cap, enforced before parsing — split the request. sync_too_large is an operation refusing synchronous JSON delivery above its published cap, with details = {sequence_length, threshold} — resubmit the identical body with Prefer: respond-async. Two operations have such a cap: the composite find-genes-and-predict-expression workflow above 50,000 bp and POST /v1/tasks/annotation/predict above 200,000 bp. The other five predict endpoints never emit it, and neither does an annotation request for ?format=bed or ?format=gff3, which stays synchronous at any accepted length. See Limits.
  • A too-long sequence is not a 413. Above 500,000 bp — or below the task's floor — is 422 validation_failed at loc ["body","sequence"], with the bound and your submitted length in the message.
  • 415 unsupported_format: an unsupported ?format= (or Accept) value is rejected outright, never silently downgraded to JSON; the message names the supported set. Formats differ per task — promoter json|bed|bedgraph, splice json|bed|gff3, enhancer json|bedgraph, chromatin json|bed, annotation json|bed|gff3, expression JSON only. Text formats are synchronous-only: pairing one with Prefer: respond-async is a 400.
  • 422 validation_failed on a closed options: every task's options object rejects unknown keys (type: "extra_forbidden", loc ["body","options","<key>"]). Notably model belongs at the top level of the body, not inside options. See Tasks.
  • 422 validation_failed on the splice response-size cap: POST /v1/tasks/splice/predict refuses a request whose response would carry more than 20,000 sites — reachable only by setting options.threshold very low (notably 0) on a long sequence, where the site count tracks sequence length rather than biology. details is {task, record_count, maximum_records, sequence_length, threshold}not the {errors: [...]} array every other validation_failed carries, and not something the OpenAPI schema declares (error responses are typed generically, so this page is the only place it is written down). The remedy is a higher options.threshold, never a shorter sequence; use 1e-3 rather than 0 if you want everything. See Limits.
  • 422 validation_failed on expression: four checks are specific to POST /v1/tasks/expression/predict — sequence below the 9,198 bp floor, sequence above 500,000 bp, tss_index missing on a sequence that is not exactly 9,198 bp, and tss_index outside [4599, len(sequence) - 4599]. There is no flag, header, or query parameter that relaxes any of them, and the tss_index pair reports at loc ["body"] (see the warning above). See Limits.
  • 429 too_many_requests: comes from three paths — the per-key concurrency cap, the per-key rate cap, or the edge per-IP cap. All emit Retry-After; the application paths also emit RateLimit-* headers. On the edge path, error.request_id is an edge-assigned identifier (32 hex chars) rather than a UUID, but still correlates with the edge access log. See Limits.

Handling errors

Three buckets:

  1. Retryable transient (too_many_requests, rate_limited, model_loading, service_unavailable, timeout, insufficient_memory): honor Retry-After; if absent, exponential backoff capped at ~30 s. For timeout and insufficient_memory, resubmit async or with a shorter sequence rather than hammering the same request.
  2. Permanent (other 4xx): surface error.message to the user; do not retry. 422 validation_failed messages are usually deterministic and safe to echo verbatim.
  3. Server bug (5xx other than 503 and 504): capture request_id and contact us. One retry is fine; tight loops are not.

504 is the exception to bucket 3: it carries no envelope and means the sync request outran the proxy window, so retry it with Prefer: respond-async rather than treating it as a server bug (see the note above).


Back to the workflow: REST API guide · Tasks.