Error handling
The status codes the gateway returns, the three error body shapes it uses, and why a blocked prompt arrives as an event on a 200 response.
- Version: 0.4
- Role: admin_user, normal_user
Errors reach you two different ways, and the difference decides how you handle them. Anything that
fails before a request enters the chat handler is an ordinary HTTP error with a status code.
Anything that fails after it arrives as an event inside a 200 OK event stream, because the
handler sets its headers before it does any work. A client that only inspects response.ok never
sees the second kind.
The error body is not uniform
There is no global exception filter in the gateway, so the body you get depends on which piece of code threw. Three shapes are in circulation.
A framework exception, such as a failed validation or a rejected token. No code field:
When the global validation pipe rejects the body, message is an array of strings, one per
failed constraint:
A guard that carries a machine-readable code and a status. These add code and sometimes
further context, and drop error:
A role rejection, which carries code and message and nothing else — not even statusCode.
Read the status from the HTTP response line, not from the body:
Write your client to treat code, statusCode and error as all optional, and to fall back to
the HTTP status when statusCode is absent.
HTTP status codes
| Code | Meaning | What to do |
|---|---|---|
400 | The body failed validation, or a required file was missing | Fix the request. message may be an array of constraint failures |
401 | No Authorization: Bearer header, or the JWT or cid_key_ was rejected | Check the credential and the Bearer prefix |
402 | LICENSE_EXPIRED — the licence is invalid or expired | An administrator installs a renewal under Settings → License. Blocks chat, guardrails, inspection, image and document analysis |
403 | A role or licence refusal — READ_ONLY_ROLE, ROLE_NOT_FOR_CHAT or FEATURE_NOT_LICENSED | Do not retry. Content blocks are not 403 |
404 | No such route, or no such resource for this tenant | Check the path prefix and the id |
415 | A legacy or macro-enabled spreadsheet was uploaded (xls, xlsm, xlsb, xlt, xltm) | Convert to xlsx; those formats cannot be redacted safely |
423 | SETUP_REQUIRED — first-boot setup is unfinished | Complete the setup wizard. Everything outside a small allowlist is locked |
429 | Too many sign-in attempts | Only POST /auth/login (10/min per IP) and the password-reset routes (5/min) are limited |
500 | An unhandled server error on a JSON route | Retry with backoff, then check the gateway logs |
503 | A dependency is unreachable — the directory at sign-in, or a helper service behind a report or analysis route | Retry later. Local sign-in is unaffected by a directory outage |
Warning
/chat/completions and /sessions/:id/messages never return 500 or 503. Both write their
SSE headers before any work happens, so a provider outage, a credential problem or an unhandled
exception all arrive as an error event on a 200 response.
Authentication errors
CombinedAuthGuard accepts either a user JWT or a gateway API key on the same header. Its
rejections all look alike:
The messages you can see are Missing or invalid authorization header, Invalid token,
Invalid or expired token, Invalid API key and Invalid or expired API key. When you get one:
- Confirm the header is
Authorization: Bearer <credential>, with theBearerprefix. There is noX-API-Keyheader. - Confirm the key still begins with
cid_key_and has not been regenerated. Regeneration invalidates the previous value at once. - Confirm the JWT has not expired. Tokens live for
JWT_EXPIRES_IN, 24 hours by default. - Confirm the endpoint accepts the credential you are using.
/sessions/*,/image-analysis/*and/document-analysis/*are JWT-only; an API key is rejected there even though it works on/chat/completionsand/models.
A 403 with code: "ROLE_NOT_FOR_CHAT" is not an authentication failure. The auditor role is
refused on chat and session writes by design, and retrying with the same token cannot succeed.
Blocked content
Content policy decisions are not HTTP errors. On the chat surface they are stream events, and on
the detection surface they are a successful response with an action field.
Chat, input rejected. The prompt never reaches the provider, so no tokens are spent:
Chat, response rejected by the output filter after the model answered. The reply is discarded and never sent:
Session messages reject differently. POST /sessions/:id/messages emits its own input
rejection with no error field at all, so a handler that keys on error misses it:
Detection endpoint. POST /api/v1/guardrails/detect returns 200 and states the verdict in
the body:
Warning
Treat action: "reject" and a content_rejected event as policy outcomes, not failures. Retrying
the identical text produces the identical verdict. Change the input, or change the filter.
Reading a stream safely
A single read from the stream can deliver several data: frames, or half of one. Buffer the
decoded text and split on the frame boundary rather than parsing what one read happened to return.
Handle four things: the terminal [DONE], an error field, the typed rejection events, and the
single content event.
If the connection drops mid-stream before the content event, you have nothing partial to salvage — the answer is emitted in one piece. Treat a truncated stream as a failed call.
Which errors to retry
| Failure | Retry | How |
|---|---|---|
| Connection refused, TLS failure, timeout before any bytes | Yes | Exponential backoff |
500 on a JSON route | Yes | Exponential backoff, then read the gateway logs |
503 on a JSON route | Yes | Exponential backoff; the dependency is down, not your request |
429 on POST /auth/login | Yes, slowly | Wait out the one-minute window. No Retry-After header is sent |
400 validation failure | No | Fix the body. Check for silently stripped unknown fields |
401 | Once | Refresh the JWT, or check the key. Repeating with the same credential cannot help |
402, 403, 423 | No | A licence, role or setup state has to change first |
415 | No | Convert the file to a supported type |
content_rejected / output_content_rejected | No | The same input produces the same verdict |
| An error event on a chat stream | Carefully | The provider may already have been billed. Retrying repeats the spend |
Note
Nothing in the gateway emits Retry-After, X-RateLimit-Limit or X-RateLimit-Remaining. Retry
logic that reads those headers is reading null on every response.
Limits and known gaps
- No uniform error envelope. Three body shapes coexist, and only guard-issued errors carry a
code. Do not build a client that requires one. - No error code taxonomy for content decisions. A block is described by an event type and a
free-text
reason, not by a stable code you can switch on. - Chat failures hide behind a 200. Monitoring that counts non-2xx responses records a provider
outage on
/chat/completionsas a success. - The two chat surfaces disagree.
/chat/completionsrejects input with anerrorfield;/sessions/:id/messagesrejects it withtype: "content_rejected"and areason. A shared client must handle both. - The generic chat failure message is opaque. An unhandled exception in the controller writes
{"error":"An error occurred"}with no detail; the cause is only in the gateway log. - No rate-limit signalling. Because only the sign-in routes are limited, there is no quota header to pace a client against on the request path.
Related pages
- Best practices — credential handling, retry policy and what the buffered stream means for your user interface.
- Integration examples — clients that implement the handling shown here.
- Chat API — the full event list for
/chat/completions.
Last updated on