Error format
Typed Z4JError failures and unexpected server failures use the small z4j
envelope below. The brain does not emit RFC 7807 Problem Details. FastAPI owns
some responses before z4j's middleware can normalize them: request-schema
validation, unknown routes, and a directly raised HTTPException retain
FastAPI's {"detail": ...} shape.
{ "error": "validation_failed", "message": "request body failed validation (2 field(s))", "request_id": "01H...", "details": { "errors": [ {"loc": "body.email", "type": "value_error.email"}, {"loc": "body.role", "type": "enum"} ] }}| Field | Meaning |
|---|---|
error |
Stable short identifier. Branch on this from API clients. |
message |
Human-readable summary. Operator-friendly English; do not parse. |
request_id |
The same value as the X-Request-Id response header. Quote it when filing a bug. |
details |
Per-error context. A Pydantic ValidationError raised inside application code is redacted to {"errors": [{"loc": "...", "type": "..."}, ...]}. FastAPI request validation uses its own detail list and may echo the rejected input; do not assume that path is secret-redacted. |
HTTP status mapping
Section titled “HTTP status mapping”The brain raises typed Z4JError subclasses; the error middleware maps them to HTTP status codes:
| Status | Exception class | Typical error codes |
|---|---|---|
| 401 | AuthenticationError, SignatureError |
unauthenticated, invalid_signature |
| 403 | AuthorizationError |
forbidden, plus mfa_reverify_required and mfa_enrollment_required for MFA gates |
| 404 | NotFoundError |
not_found |
| 409 | ConflictError |
conflict |
| 422 | ValidationError, InvalidFrameError |
validation_error, invalid_frame; application-raised Pydantic validation uses validation_failed, while FastAPI request validation uses detail |
| 426 | ProtocolError |
protocol_incompatible or protocol_version_mismatch; mapped but not currently emitted by an HTTP route |
| 429 | RateLimitExceeded |
rate_limited |
| 502 | AdapterError |
adapter_error; mapped but normal adapter failures arrive asynchronously as a failed command result |
| 503 | AgentOfflineError |
agent_offline; local-registry command dispatch can emit this, while PostgreSQL coordination can instead leave a command pending until its deadline; schedule trigger reports no online agent as 404 |
| 504 | CommandTimeoutError |
command_timeout; mapped but no HTTP endpoint emits it -- the sweeper marks the command timed out for later polling |
| 500 | everything else | internal_error (with request_id for log correlation; the exception class name is deliberately not surfaced) |
Tracing
Section titled “Tracing”Responses that pass through the request-id middleware include an
X-Request-Id header, and z4j error envelopes repeat it as request_id. An
early body-size rejection is an exception: it can return 413 before that
middleware runs, with no header and a null body field. FastAPI detail
responses do not add a request_id body field. Pass the header when it is
present when filing a bug or scanning logs.
Audit trail for denials
Section titled “Audit trail for denials”Authentication, authorization, not-found, and validation failures on
mutating schedule routes (POST, PUT, PATCH, DELETE) are offered to a
bounded best-effort audit queue so IDOR-enumeration attempts can leave a
forensic breadcrumb. Reads are not covered, nor are other API families. Queue
overflow or a persistence failure can drop the breadcrumb without changing the
client's error response. Rows that are written participate in the HMAC chain;
structured log lines alone are not tamper-evident.