Errors
Every failure comes back in one envelope, whichever route refused it:
{
"error": {
"type": "https://reelwire.io/errors/forbidden",
"code": "forbidden",
"message": "This key may not read here. Give it the \"Team: read\" permission, or use a key that has it.",
"requestId": "01M2SR17R25EKN6DM1H9DVDS0D",
"details": []
}
}
The fields, and what each is for:
| Field | What it is |
|---|---|
code | The machine-readable reason. Branch on this, never on the message. |
type | A stable documentation URI, https://reelwire.io/errors/{code}. It carries no extra information beyond the code. |
message | Written for a person to read and safe to show in your own interface. It never carries a stack trace or a secret. |
requestId | A ULID that correlates this failure with the log line in every container the request touched. The same value is on the x-request-id response header of every response, successful or not. |
details | One entry per problem, each with a JSON Pointer into the request body. |
retryAfterSeconds | Present on rate_limited and upstream_unavailable. |
Quote the requestId when you ask about a failure. It is the one value that finds the request
again.
Details carry a pointer per problem
details is what makes a refusal actionable: a client can point at the exact field rather than
print a paragraph and leave somebody to find it.
curl http://localhost:4000/v1/stream \
-H "Authorization: Bearer rw_your_key_here" \
-H "Content-Type: application/json" \
-d '{"streamId":"st_30472168d941","eventId":"not-a-ulid","occurredAt":"yesterday","data":{}}'
{
"error": {
"type": "https://reelwire.io/errors/schema_validation",
"code": "schema_validation",
"message": "Stream envelope is not valid",
"requestId": "01M2SQNH1MDKPWQ83Y3ECG60M3",
"details": [
{ "path": "/eventId", "message": "must be a Crockford base32 ULID", "rule": "invalid_string" },
{ "path": "/occurredAt", "message": "Invalid datetime", "rule": "invalid_string" }
]
}
}
path is an RFC 6901 JSON Pointer into the body you sent. An empty string means the document as a
whole. rule is an optional machine hint, and expected and received appear where the checker
knew both.
The codes, and the status each carries
A route never picks its own status: the code decides it, in one table.
| Code | Status | When |
|---|---|---|
malformed_json | 400 | The body is not JSON. |
schema_validation | 400 | Well-formed JSON that does not fit the shape. |
ratio_not_supported | 400 | The template does not come in that shape. |
locale_not_supported | 400 | Nothing is drawn in that language. |
blocked_url | 400 | A URL that Reelwire will not fetch or deliver to. |
unauthenticated | 401 | Missing or invalid credentials. |
signature_invalid | 401 | A signature that does not check out. |
timestamp_skew | 401 | A signed request too far from now. |
forbidden | 403 | A valid credential that may not do this. |
not_found | 404 | No such thing, or not yours. |
unknown_source | 404 | No shipped feed under that source slug. |
unknown_format | 404 | No such format under that source. |
unknown_template | 404 | No such template. |
unknown_stream | 404 | No stream with that id on this account. |
conflict | 409 | A conflict with what is already there. |
idempotency_conflict | 409 | The same eventId with a different body. |
invalid_state_transition | 409 | The thing is not in a state this action applies to. |
stream_disabled | 409 | The stream exists and is switched off. |
no_sample | 409 | Asked about a stream's own field names before it has received anything. |
template_not_approved | 409 | The template is not cleared for this workspace. |
replayed | 409 | A delivery id that has already been processed. |
payload_too_large | 413 | Over the size limit for this route. |
unsupported_media_type | 415 | The wrong Content-Type. |
rate_limited | 429 | Too many requests. retryAfterSeconds says how long to wait. |
internal | 500 | A fault on Reelwire's side. |
render_failed | 502 | The renderer refused the job. |
asset_unavailable | 502 | A file the render needed could not be fetched. |
checksum_mismatch | 502 | A file arrived but is not the file that was promised. |
upstream_unavailable | 503 | A dependency is down, such as every renderer being unreachable. |
render_timeout | 504 | The renderer took too long. |
One code is deliberately not a failure. stale_event is served with 202: the event was well
formed and was stored, it was older than the feed accepts, so nothing was drawn. A producer
replaying history sees success rather than a wall of 4xx that would hide a real error among it.
404 and 403 are not interchangeable
A thing that belongs to another workspace answers exactly as a thing that does not exist. That is
on purpose: telling a stranger that st_000000000000 exists but is not theirs is telling them
something. A 403 means the credential is yours and is not allowed the action, and the message names
the permission to add. See Permissions.
Retrying
429 and 503 are worth retrying, after retryAfterSeconds where it is given and with a backoff
otherwise. 502 and 504 on a render are worth one retry. Everything else in the 4xx range is
not: the request will be refused the same way until it changes.
Sends carry an eventId that makes a retry idempotent, so retrying a timeout cannot post twice.
See Sending events.