Error Model
All API errors use a consistent JSON envelope. This means your client code can handle errors the same way regardless of whether they come from auth, validation, storage, or business logic.
Error envelope
{
"schema_version": 1,
"error": {
"code": "not_found",
"message": "Repository abc123 not found",
"retryable": false,
"details": {}
}
}
| Field | Type | Description |
|---|---|---|
schema_version |
integer | Always 1 for now. Will increment if the envelope shape changes. |
error.code |
string | Stable machine-readable error category. Use this for branching in client code. |
error.message |
string | Human-readable explanation. Safe to display directly to the user. |
error.retryable |
boolean | true if retrying the same request might succeed (e.g., a transient dependency error). false if the request will always fail until something changes (e.g., invalid input). |
error.details |
object | Optional structured context, e.g., which field failed validation. |
How to use this in client code
const response = await fetch("/v1/repositories/" + repoId);
if (!response.ok) {
const body = await response.json();
const { code, message, retryable } = body.error;
if (code === "not_found") {
showError("Repository not found");
} else if (retryable) {
scheduleRetry();
} else {
showError(message); // safe to show directly
}
}
Don't parse exception text or rely on the HTTP status code alone — use code for any branching that needs to be specific.
Common error codes
| Code | HTTP status | What it means |
|---|---|---|
invalid_request |
400 |
The request body or parameters are malformed |
validation_error |
422 |
Input failed validation (wrong type, missing field, etc.). details contains field-level errors. |
invalid_token |
401 |
The Bearer token is missing, expired, or invalid |
forbidden |
403 |
The user doesn't have the required group membership |
not_found |
404 |
The requested resource doesn't exist or isn't accessible to this user |
conflict |
409 |
The operation conflicts with existing state (e.g., scan already running) |
quota_exceeded |
429 |
The user has hit a quota limit (e.g., max active scans) |
service_unavailable |
503 |
A required dependency (Postgres, S3, SQS) is unreachable. retryable: true. |
internal_error |
500 |
Unexpected error. retryable may be true for transient failures. |
Backend implementation
The api_error() helper in app/core/errors.py creates this envelope. Route handlers and services should use this helper rather than raising raw HTTP exceptions, to ensure consistent formatting.
A note on 422 responses
FastAPI automatically generates 422 Unprocessable Entity responses for Pydantic validation failures. These have a slightly different structure because FastAPI produces them before reaching the error helper. The details field will contain FastAPI's validation error list with field paths and messages.