The AILA REST API ships two coexisting non-2xx envelopes. Which one a response uses depends on the exception class raised by the route, not the status code. Both are documented here.
Returned by the Phase 80 handlers in src/aila/api/app.py and the
_catch_unhandled_exceptions middleware:
{
"detail": "Human-readable error message",
"code": "MACHINE_READABLE_CODE_OR_NULL",
"errors": null
}code may be null for status codes where the API has no machine code
(401, 403, 404, 409 -- see the catalog below). errors is null except for
validation errors that flow through the Phase 80 handler.
Returned by register_error_handlers()
(src/aila/api/errors/handlers.py) for every AILAError subclass, every
fastapi.RequestValidationError, and any otherwise-unhandled Exception:
{
"code": "MISSING_API_KEY",
"message": "LLM API key is not configured.",
"hint": "Go to Admin -> API Keys and add the provider key for this operation.",
"trace_id": "5e7c1c4f3f8d4a4c8b9c0d1e2f3a4b5c"
}codecomes from the exception'sClassVar codefor typed subclasses, or a derived_DERIVED_NAME_ERRORtoken for pre-Phase-176a subclasses that fall back to HTTP 500.messageis always a safe static string sourced from the exception'sClassVar user_message(typed taxonomy) or"An internal error occurred."for any 500-class path.str(exc)is never placed inmessage-- it could leak file paths, provider identifiers, or other caller-supplied context (Phase 178 S1).hintresolves throughERROR_HINTSinsrc/aila/api/errors/hints.py; falls back to theDEFAULTentry when no code-specific hint is registered.trace_idis the currentcorrelation_idcontextvar set byCorrelationIdMiddleware;Nonewhen the exception fires before that middleware binds the context.
The Phase 178 redactor safe_exc_message() in
src/aila/platform/workflows/log.py enforces the matching redaction for
the audit log: exception text persisted to workflow_state_transitions is
replaced with type(exc).__name__ unless the exception inherits from
WorkflowSafeMessage. Full stack traces always land in structlog
server-side via logger.exception(...).
code |
HTTP | Exception | When |
|---|---|---|---|
MISSING_API_KEY |
503 | MissingApiKeyError |
LLM/provider API key is not configured. |
SSH_CONNECTION_FAILED |
502 | SSHConnectionFailedError |
SSH to a target system fails. |
ROUTER_ERROR |
500 | RouterError |
Internal LLM/OmniRoute routing failure. |
MODULE_PLATFORM_NOT_READY |
503 | ModulePlatformNotReadyError |
A module runtime is still initializing. |
CONFIG_VALUE_MISSING |
500 | ConfigValueMissingError |
A required ConfigRegistry entry is absent. |
WORKER_UNREACHABLE |
503 | WorkerUnreachableError |
The background task worker is unreachable. |
VALIDATION_ERROR |
422 | RequestValidationError |
Request body or query/path params fail Pydantic validation. |
INTERNAL_ERROR |
500 | any otherwise-unhandled Exception |
The generic last-resort handler. |
| (derived) | 500 | any legacy AILAError (AuthenticationError, NotFoundError, …) |
Subclass lacks the ClassVar code / http_status pair; the handler derives a code from the class name and returns 500. |
The legacy catalog below documents the ErrorResponse shape used by every
route that raises HTTPException directly.
- When: No
Authorization: Bearer <token>header, or the token is malformed. - Response:
{"detail": "Not authenticated"} - Fix: Present a valid Bearer JWT from
POST /auth/login(user) orPOST /auth/token(API key).
- When:
POST /auth/loginwith an unknown username, an inactive account, an OIDC-only account, or a wrong password. - Response:
{"detail": "Invalid credentials"} - Fix: Verify username + password. The same string is returned for every failure mode to avoid username enumeration; check the server audit log for
login_failedwith the specific reason.
- When:
POST /auth/refresh/userwith a missing, expired, revoked, or wrong-type refresh token, or whose user account is no longer active. - Response:
{"detail": "Invalid or expired refresh token"} - Fix: Re-authenticate via
POST /auth/login.
- When: JWT
expclaim has passed. - Response:
{"detail": "JWT access token has expired -- obtain a new token via POST /auth/token or POST /auth/refresh"} - Fix: Refresh via
POST /auth/refresh(API key) orPOST /auth/refresh/user(user), or re-authenticate.
- When: The
ApiKeyRecordmatched by the JWT'skey_idclaim hasrevoked_atset (zero-cache-window blacklist). - Response:
{"detail": "API key has been revoked"} - Fix: Create a new API key via
POST /auth/keys(admin) oraila create-api-key(CLI).
- When:
POST /auth/tokenwith a raw key that does not match any activeApiKeyRecordhash. - Response:
{"detail": "Invalid API key -- verify the key is correct and not revoked, then retry POST /auth/token"} - Fix: Verify the key value or create a new key.
- When: slowapi rate limit triggered for the bucket derived from the JWT
user_id/key_idclaim or remote IP (e.g. >10 logins/min, >5 token exchanges/min). - Response: slowapi handler body (JSON with
error+Retry-Afterheader). - Fix: Back off and retry after the duration in
Retry-After. For per-user limits, the bucket is identity-based, so a noisy peer cannot starve other callers.
- When: Endpoint requires a higher role than the caller has (e.g., reader calling admin-only endpoint)
- Response:
{"detail": "Insufficient permissions: requires <role> role"} - Fix: Use a token from a key with the required role
| Endpoint Category | Required Role |
|---|---|
POST /auth/keys, DELETE /auth/keys/{id} |
admin |
PUT /config/{ns}/{key} |
admin |
POST /analyze, POST /task |
operator |
All GET endpoints, POST /sessions |
reader |
- When: Request body fails Pydantic schema validation (missing fields, wrong types, constraint violations)
- Code:
VALIDATION_ERROR - Response:
{
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"hint": "Fix the highlighted input fields and retry.",
"trace_id": "5e7c1c4f3f8d4a4c8b9c0d1e2f3a4b5c"
}Phase 176a routes validation errors through validation_error_handler,
which emits the ErrorEnvelope shape above. Pre-Phase-176a code paths
that raise a custom 422 via HTTPException still return the older
ErrorResponse shape with detail + errors populated. Refer to the
route's OpenAPI documentation for the exact shape it returns.
- Fix: Match the request body against the schema published at
/docs.
- When:
PUT /config/{namespace}/{key}with a value that fails ConfigRegistry validation - Response:
{"detail": "Invalid value for {namespace}.{key}: {reason}"} - Fix: Check the expected type and constraints for the config field
- When: Requested resource does not exist or is not accessible to the caller
- Affected endpoints:
GET /scans/{run_id},GET /tasks/{task_id},GET /sessions/{id}/messages,GET /systems/{id} - Response:
{"detail": "Resource '{id}' not found or not accessible"} - Fix: Verify the resource ID; check that the caller has access (same group_id, or admin role)
- When: Session ID does not exist or belongs to another user (D-25: user isolation)
- Response:
{"detail": "Session '{id}' not found or belongs to another user"} - Fix: Verify the session_id via
POST /sessions
- When:
POST /tasks/{id}/cancelon a task in done/failed/cancelled state - Response:
{"detail": "Task '{id}' is already in a terminal state"} - Fix: Only cancel non-terminal tasks (queued, waiting, running, paused)
- When:
POST /tasks/{id}/resumeon a task not in paused state - Response:
{"detail": "Task '{id}' is not in PAUSED state"} - Fix: Only resume paused tasks
- When:
DELETE /auth/keys/{id}on an already-revoked key - Response:
{"detail": "Key already revoked"}
- When: Task submission creates a dependency cycle
- Response:
{"detail": "Dependency cycle detected"} - Fix: Remove the circular dependency from
depends_on
- When: Request body exceeds 10 MB (Content-Length header check in
_reject_oversized_requests). - Response:
{"detail": "Request body too large (max 10MB)", "code": "PAYLOAD_TOO_LARGE", "errors": null} - Fix: Reduce payload size; for bulk operations, use smaller batches.
- When:
POST /analyze,POST /task, or session message endpoints called before platform startup completes - Response:
{"detail": "Platform not initialized -- check server logs for startup errors and restart the API server"} - Fix: Wait for startup to complete; check server logs for initialization errors
- When: The exception bubbles past every typed handler.
- Response (
ErrorEnvelopepath):{"code": "INTERNAL_ERROR", "message": "An internal error occurred.", "hint": "...contact support with the trace ID shown below.", "trace_id": "..."} - Response (legacy
_catch_unhandled_exceptionspath):{"detail": "Internal server error", "code": null, "errors": null} - Fix: Capture the
trace_idand correlate against server-side structlog.safe_exc_message()redacts persisted audit text to the exception class name; full traceback only ever reaches structlog.
register_error_handlers() covers AILAError, RequestValidationError,
and bare Exception. The middleware-level _catch_unhandled_exceptions
exists as a belt-and-suspenders 500 wrapper for any path that escapes the
handler chain (e.g. errors raised in middleware itself).
- When: SSE endpoint called but
AILA_PLATFORM_REDIS_URLis not set - Response: Single SSE event then close:
data: {"message": "Redis not configured \u2014 no progress stream available"}
- Fix: Configure Redis URL via
AILA_PLATFORM_REDIS_URLenv var orPUT /config/platform/redis_url
| Status | Meaning | Common Causes |
|---|---|---|
| 200 | Success | GET, PUT, POST (sync responses) |
| 201 | Created | POST /sessions, POST /auth/keys |
| 202 | Accepted | POST /analyze, POST /task (async) |
| 401 | Unauthorized | Missing/invalid/expired/revoked token |
| 403 | Forbidden | Insufficient role for endpoint |
| 404 | Not Found | Resource missing or not accessible |
| 409 | Conflict | Terminal state, already revoked, cycle |
| 413 | Payload Too Large | Request body > 10 MB |
| 422 | Validation Error | Schema validation failure (ErrorEnvelope, code=VALIDATION_ERROR) |
| 429 | Too Many Requests | slowapi rate limit triggered |
| 500 | Internal Error | Unhandled exception (ErrorEnvelope, code=INTERNAL_ERROR) |
| 503 | Service Unavailable | Platform not initialized |
Source: src/aila/api/errors/ (envelope handlers + hint registry),
src/aila/platform/exceptions.py (typed taxonomy),
src/aila/api/app.py (Phase 80 handlers + middleware),
src/aila/platform/workflows/log.py (safe_exc_message).