Handle errors¶
When Prelum cannot render a request, it returns JSON describing what went wrong. Alongside the HTTP
status and stable code, every response names origin: whose failure this is, so a client can
decide without keeping its own list of codes — see "Whose failure it is" below for the detail. The
human-readable detail field is for showing the user, not for branching on.
Typical responses fall into these groups:
| Status | What it usually means | What the client should do |
|---|---|---|
| 400 or 422 | The request, file data or Typst source is invalid | Correct the request before trying again |
| 403 | The API token is missing or incorrect | Fix the client configuration |
| 408 | Typst exceeded the render timeout | Simplify the document or ask the operator about the limit |
| 413 | An input or output limit was exceeded | Read code and context to find the exact limit |
| 429 | All render slots remained busy | Wait for the Retry-After duration, then retry |
| 500 or 503 | Prelum or its infrastructure failed | Retry cautiously and alert the service operator |
Example response¶
Every error uses application/problem+json and has the same top-level fields:
{
"code": "template_file_too_large",
"origin": "template",
"title": "Template Too Large",
"status": 413,
"detail": "Template file 'assets/logo.png' size 1500000 bytes exceeds limit 1048576",
"instance": "https://prelum.example/v1/render",
"context": {
"key": "assets/logo.png",
"size": 1500000,
"limit": 1048576
}
}
In application logic, compare code rather than title or detail: those two fields are prose and
may be reworded. context contains details such as the measured size, configured limit or affected
file key. It is {} when the error needs no additional values.
Several different conditions use status 413, so the status alone cannot tell a client what was too
large. The code distinguishes the request body, source, one file, one data string and the rendered
output.
Fields may be added¶
Request objects are strict: an unknown field is a mistake and is rejected rather than ignored.
Responses are the opposite. Any response body — an error, or the /v1/constraints document — may
gain fields within the same API version, so a client must ignore the ones it does not recognise, and
a validator that rejects unknown members will break on an ordinary release. The published schema
says so in both directions: every response schema is declared open, every request schema closed.
What will not change without a new API version: an existing field disappearing, changing type or
changing meaning, an existing code answering a different status, or a documented origin value
being withdrawn.
Context fields¶
A field means the same thing wherever it appears.
Caller text in prose, validation errors and path escapes lone surrogates as literal \uXXXX
text before shortening, so these fields can always be encoded as UTF-8.
| Field | Type | Meaning |
|---|---|---|
limit |
integer | The configured limit that was exceeded, in the unit the code counts |
size |
integer | A measured size in bytes |
count |
integer | A measured or requested number of things |
key |
string | One files key, complete and exactly as sent |
rule |
string | The id of the rule behind the failure detail reports — a whole-set files-key rule or an output-option rule — as published by GET /v1/constraints. Branch on it rather than on detail. A request breaking an identified rule and something the rules do not name reports the latter, and carries no rule until it is fixed |
path |
array | Where a value sits in data: object keys as strings, array indices as integers. String segments longer than 80 characters are shortened with … |
subject |
string | key or value: which string at path is at fault. An oversized object key is its own last path segment, so the path alone cannot say |
declared_size |
integer | The Content-Length the caller sent; not a measurement |
timeout_secs |
integer | The configured render timeout in seconds |
retry_after |
integer | Seconds to wait before retrying, equal to the Retry-After header |
errors |
array | Request validation errors as {loc, msg, type}, at most 20 of them. loc is shortened like path: string segments over 80 characters end in …, array indices stay integers. msg is prose that may quote the offending value, and is shortened the same way past 200 characters. Like detail, it may be reworded — including its leading words — so branch on type, or on context.rule where one is given |
errors_total |
integer | How many validation errors there were, present only when errors holds fewer than that |
Codes¶
| Status | code |
origin |
context |
Cause |
|---|---|---|---|---|
| 400 | invalid_request |
request |
errors, absent when the body could not be parsed at all; rule when an output-option rule rejected it |
Missing, malformed, unknown or format-inappropriate request field, or a body no JSON parser accepts |
| 400 | unsupported_format |
request |
errors |
format outside pdf, svg and png |
| 400 | invalid_template_path |
request |
errors |
Unsafe, non-normalised or otherwise invalid files key |
| 400 | invalid_file_data |
request |
key or count + limit, rule when a whole-set rule rejected it, and errors only when validation raised it (see below) |
Malformed base64, excessive or colliding entries, or non-UTF-8 text |
| 403 | forbidden |
request |
— | Missing or incorrect API token |
| 404 | not_found |
request |
— | No route at the requested path |
| 405 | method_not_allowed |
request |
— | The route exists but not for this method; Allow names the ones it has |
| 408 | render_timeout |
request |
timeout_secs |
Typst exceeded the render timeout |
| 413 | request_too_large |
request |
limit, declared_size when Content-Length was sent |
JSON body exceeded the request-body limit |
| 413 | template_source_too_large |
template |
size, limit |
source exceeded its limit |
| 413 | template_file_too_large |
template |
key, size, limit |
One files entry exceeded its limit after decoding |
| 413 | string_too_large |
request |
path, subject, size, limit |
One string in data exceeded its limit |
| 413 | output_too_large |
request |
size, limit |
Direct output, aggregate images or ZIP exceeded the output limit |
| 413 | page_selection_too_large |
request |
count, limit |
pages selects more pages than an archive may hold; nothing was rendered |
| 413 | too_many_output_files |
request |
limit |
The document produced more pages than an archive may hold |
| 422 | template_compile_failed |
template |
— | Caller-supplied Typst or page selection could not produce the requested output, including a render killed for exhausting memory |
| 429 | render_queue_full |
capacity |
retry_after |
No render slot became free before the queue deadline |
| 500 | render_failed |
service |
— | Typst was killed by a signal the render's own memory limit does not explain, or reported success but produced no expected output |
| 503 | service_unavailable |
service |
— | Unexpected service or infrastructure failure |
invalid_file_data carries key when one files entry is at fault — malformed base64, text that
is not UTF-8, a key that escapes the project root or collides with another in case or as a
directory, or a layout that cannot be written — and count with limit when there are too many
entries. Non-UTF-8 source carries neither. errors accompanies these fields only for the causes
request validation detects (the key collisions and the structural key-count cap); the causes the
renderer detects have no validation error list to publish. invalid_template_path never carries the key: it has not passed validation, so it is not
echoed; detail names it, shortened.
A 429 includes Retry-After; callers should wait at least that many seconds before retrying.
Whose failure it is¶
origin says what a failure is attributable to, so a client decides with one field instead of
keeping its own list of codes. A code added later classifies itself on the day it ships.
origin |
Meaning | What the client should do |
|---|---|---|
request |
The request itself, or the document it asks for | Correct the request |
template |
The inline template the request shipped | Correct the template |
capacity |
Prelum is shedding load by design; nothing is broken | Wait for Retry-After, then retry |
service |
Prelum itself failed | Retry cautiously and alert the service operator |
service is the only value that means Prelum failed, and it is the only one worth alerting on.
capacity in particular is designed behaviour: a busy service is not a broken one, and a client
that pages on it pages on ordinary load.
request and template are both the client's side of the exchange — source and files arrive in
the same body — so the split says which half to correct, not which is more serious. Neither is ever
Prelum's failure.
The complete mapping is published as error-codes.json: every code with its
status and origin, generated from the service's own definitions.
Privacy of error responses¶
Request bodies are never echoed. Typst diagnostics are neither returned nor logged because they can quote caller-supplied source. Caller-controlled paths and validation messages are shortened and made safe for UTF-8 before appearing in a response; see the context descriptions above for the few bounded values that are returned exactly.