Errors
Every status and error code this API can return, and what each one means.
Every refusal from this API is a specific error code, never a bare non-2xx status with nothing
to branch on. The response body is always { "error": "<code>", "message": "<human text>" }, with
target-unreachable carrying three extra fields — see below.
This table is generated from — and kept in sync with, by a CI test — the ERROR_TABLE map and the
router’s own responses in this API’s source (apps/api/src/errors.ts, apps/api/src/compress.ts,
apps/api/src/form.ts, apps/api/src/auth.ts, apps/api/src/index.ts). If this page and that
source ever disagree, the source is right and this page has a bug.
| Status | Code | Meaning |
|---|---|---|
| 400 | bad-request |
a malformed request: target missing or not a parseable size, format not one of keep/jpeg/png/webp, allow_downscale not true/false, file field missing or not a file, or a multipart body that couldn’t be parsed at all |
| 400 | empty-file |
the uploaded file is a real file field, but it’s 0 bytes |
| 401 | unauthorized |
the Authorization header is missing, malformed, or doesn’t match a known key — these three cases are indistinguishable in the response on purpose, see Authentication |
| 404 | not-found |
the route doesn’t exist (only POST /v1/compress, GET /v1/health, and OPTIONS /v1/compress are real) |
| 411 | length-required |
no Content-Length header was sent — refused before the body is ever read, since there’s no way to enforce the byte cap on a body of unknown size |
| 413 | input-too-large |
the upload exceeds MAX_INPUT_BYTES — refused from Content-Length alone when possible, before the body is buffered; see Limits |
| 415 | unsupported-format |
the file isn’t a recognisable image, or it’s HEIC/AVIF — this endpoint refuses both explicitly, see Limits |
| 422 | decode-failed |
bytes passed format sniffing but the decoder rejected them (truncated or corrupt file) |
| 422 | dimension-exceeded |
decoded dimensions exceed MAX_MEGAPIXELS, checked from the header before a full decode |
| 422 | target-unreachable |
the search couldn’t reach target — an answer, not a failure, see below |
| 429 | rate-limited |
this key has exceeded 10 requests / 60 s — response carries a Retry-After header with the seconds left in the key’s own window |
| 500 | internal |
an unexpected failure — either a genuine bug, or this Worker’s own configuration is broken (a missing/invalid wrangler.jsonc var); the message names exactly which |
| 503 | processing-timeout |
the request’s abort signal fired — read Limits before assuming this fires reliably on every slow request |
| 503 | rate-limiter-unavailable |
the per-key rate limiter itself could not be reached, so the request was refused rather than served unmetered — the limiter fails closed on purpose, see Authentication; carries Retry-After: 60 |
target-unreachable is an answer, not a failure
When the byte-target search can’t land target at all, the response is still 422, but the body
carries three extra fields beyond error/message:
{
"error": "target-unreachable",
"message": "…",
"reason": "FORMAT_FLOOR",
"closest": 61440,
"suggestions": [
{ "kind": "switch-format", "format": "webp" },
{ "kind": "raise-target", "bytes": 65536 }
]
}
reasonis one ofFIXED_DIMENSIONS,MIN_DIMENSION,FORMAT_FLOOR, orBUDGET_EXHAUSTED— the same four-way vocabulary the CLI’s exit codes 10–13 and the library’s give-up codes use. See that page for what each one means and what lever fixes it.closestis the byte size of the smallest file the search actually produced — a real, complete result you can choose to accept even though it’s overtarget.suggestionsis the same structured-data vocabulary the CLI’s own stderr sentences and--jsonoutput are built from (packages/cli/src/messages.ts), so a caller scripting against both surfaces never has to parse two different descriptions of the same outcome.
Four codes you’ll never see from this API
The engine (@imagekbreducer/image-core) defines nine ErrorCode values in total; this API’s
ERROR_TABLE covers all nine, but four of them are folded into the plain 500 internal above
rather than getting their own caller-facing code:
encode-format-mismatchandno-canvas-surfaceare canvas-backend-only failures (a browser<canvas>returning the wrongblob.type, or noOffscreenCanvas/documentto draw on). This API runs entirely on the wasm backend — the same one the CLI uses — and never constructs a canvas backend, so neither can actually be thrown here. They’re mapped only so the engine’s fullErrorCodeunion type-checks against this API’s error table; if either ever did fire, that would itself be a bug worth a 500.encode-failedandinvalid-opare reachable through this API’s wasm backend, but neither is caused by anything a/v1/compressrequest supplies — the request body is onlyfile/target/format/allow_downscale, with no crop/rotate/pad parameters for a caller to get wrong. Anencode-failedorinvalid-ophere can only be this API’s own encoder or geometry math failing on otherwise-valid input, which is what500 internalis for.
If you’re comparing this table against the CLI’s exit codes: the CLI gives
invalid-op its own exit code (25), because a CLI user reaches it directly via --crop/--pad
flags that this API doesn’t expose. Same engine code, different surface, different honest answer.