Errors¶
Errors are RFC 9457 problem documents, served as
application/problem+json:
{
"type": "https://docs.trilocore.ai/errors/not-found",
"title": "Not found",
"status": 404,
"detail": "no such session",
"code": "not_found",
"error": "not_found",
"message": "no such session",
"instance": "/api/v1/bevm/sessions",
"request_id": "req_e345361146654a96898ff450881779d0"
}
Every error carries a request_id that matches the x-request-id response header. Log it. It is
the one field that makes a support conversation tractable.
The fields¶
| Field | Type | Notes |
|---|---|---|
code |
string | the machine contract. Stable lower_snake_case; branch on this |
status |
number | repeats the HTTP status |
title |
string | short, stable, human-readable summary for a given code |
detail |
string | varies per occurrence; safe to show a user, unsafe to parse |
type |
string (URI) | a documentation URL derived from code. May move — do not branch on it |
instance |
string | the request path that produced the error |
request_id |
string | equals the x-request-id header |
error |
string | legacy alias, always equal to code |
message |
string | legacy alias, always equal to detail |
code is the contract, not type and not title. type is a documentation URL and may be
re-pointed; title and detail are prose and may be reworded. Some errors also merge extra
top-level members — quota_exhausted carries used, limit, plan and reset_at;
method_not_allowed carries allow.
The catalogue¶
| Status | code |
Meaning |
|---|---|---|
400 |
validation_failed |
the request is malformed — most often a path parameter of the wrong shape |
400 |
idempotency_key_required |
the endpoint requires Idempotency-Key and it was absent |
401 |
unauthorized |
missing, malformed, expired or revoked credential |
403 |
forbidden |
authenticated, but not permitted to perform this operation |
403 |
csrf_required |
cookie-authenticated writing method with no x-csrf-token header, on a proxied prefix. On a gateway-native prefix (/api/v1/notes/*) the same rejection arrives as forbidden with csrf_required in detail — match the 403, not one code string |
403 |
persona_forbidden |
the credential's persona may not use this endpoint |
404 |
not_found |
the resource does not exist, or is not yours |
405 |
method_not_allowed |
wrong method for this path; the response carries an accurate Allow header and an allow array |
409 |
conflict |
the request conflicts with the resource's current state |
412 |
precondition_failed |
an If-Match / If-None-Match precondition did not hold |
413 |
payload_too_large |
the body exceeds the limit for this surface |
428 |
precondition_required |
this operation requires a conditional request; send If-Match |
429 |
rate_limited |
too many requests — see Rate limits |
429 |
quota_exhausted |
the plan's daily allowance is spent — see Rate limits |
500 |
internal |
an unexpected failure inside the API |
502 |
upstream_unavailable |
the service behind the front door did not answer |
Three further codes come from the idempotency layer and are covered in
Idempotency: invalid_idempotency_key, idempotency_key_reuse and
idempotency_conflict.
The catalogue is closed and append-only. New codes may be added; existing ones do not change
meaning. Write your client so an unrecognised code falls back to branching on the HTTP status.
Reading an error safely¶
Not every error is a problem document. Three of the idempotency errors are returned as plain
application/json with a minimal body and no code member:
A client that reads only body.code sees undefined for those three and falls through to its
"unknown error" branch — which, for a 409 raised by a request that is still in flight, usually
means retrying something it should have waited on.
Read the identifier defensively. This one expression is correct for every error the API returns,
because problem documents carry error as a legacy alias of code:
Do not sniff Content-Type for exactly application/json to decide whether a body is an error —
application/problem+json will fail that test, and it parses as ordinary JSON anyway.
What errors deliberately do not tell you¶
Three behaviours look like bugs and are not:
404 where you expected 403. A resource that exists but belongs to someone else is
reported exactly like one that does not exist. The API will not confirm another tenant's
resources to you, so absence and denial are intentionally indistinguishable.
401 where you expected 404. Authentication is checked before the path is resolved, so an
unauthenticated request to a path that does not exist answers 401. Route existence is not
disclosed to anonymous callers. Authenticate before concluding an endpoint is missing.
Terse 500 and 502 details. The detail on a server-side error is a fixed string.
Upstream URLs, internal hostnames and database text never reach a client. When you need to
escalate one of these, the request_id is what identifies it — the body deliberately carries
nothing else of diagnostic value.
Retrying¶
| Status | Retry? |
|---|---|
400, 403, 404, 405, 412, 413, 422 |
No. Fix the request; the same request will always fail |
401 |
Only after refreshing the credential — otherwise no |
409 conflict |
Only if your own state changed |
409 idempotency_conflict |
Yes, after retry-after |
428 |
Yes, once, with the required precondition header |
429 |
Yes, after retry-after — see Rate limits |
500, 502 |
Yes, with exponential backoff and jitter |
Any retry of a POST should carry the same Idempotency-Key as the original attempt. Note that
a response with a status of 500 or above is never stored for replay, so a retry after one of
those genuinely re-executes.