1. Overview and Design Goals
Section titled “1. Overview and Design Goals”Sillo’s exception handling subsystem converts Python exceptions raised during request processing into well-formed HTTP responses (or WebSocket close frames). The design achieves three things:
- Uniform error contract: Every error response from the framework follows a predictable JSON shape so API clients never need to guess.
- Two-tier dispatch: Status-code-based handlers (fast, integer-key lookup)
are tried first for
HTTPExceptioninstances; class-based handlers (MRO walk) handle everything else. - Polymorphic fallback. The MRO walk means registering a handler for a base class automatically covers all subclasses unless a more specific handler is registered.
Design Principles
Section titled “Design Principles”| Principle | How it manifests |
|---|---|
| Fail closed | Unhandled exceptions are logged with full traceback and re-raised; the server error middleware catches them. |
| Content negotiation | handle_404_error inspects the Accept header: browsers get HTML, API clients get JSON, fallback is plain text. |
| No information leaks in production | Debug mode is opt-in; generic messages are the default. The ResponseValidationError handler deliberately omits the offending value. |
| Explicit over implicit | The two registries are separate dictionaries. Developers choose whether to match by status code or exception class. |
| Single source of truth | Validation errors always carry location-prefixed loc arrays so clients know which request part failed. |
2. Exception Hierarchy
Section titled “2. Exception Hierarchy”Sillo defines three top-level exception classes, each in its own module. The full inheritance tree looks like this:
graph TD
EX["Exception"]
EX --> HTTP["HTTPException<br/><i>sillo/exceptions.py</i>"]
EX --> RVE["RequestValidationError<br/><i>sillo/validation/errors.py</i>"]
EX --> RSE["ResponseValidationError<br/><i>sillo/validation/errors.py</i>"]
EX --> WSE["WebSocketException<br/><i>sillo/exceptions.py</i>"]
HTTP --> NF["NotFoundException<br/><i>sillo/exceptions.py</i>"]
HTTP --> AUTH["AuthException<br/><i>sillo/auth/exceptions.py</i>"]
AUTH --> AF["AuthenticationFailed"]
AUTH --> PD["PermissionDenied"]
classDiagram
class Exception {
<<Python builtin>>
}
class HTTPException {
+int status_code
+Any detail
+dict headers
+__str__() str
+__repr__() str
}
class NotFoundException {
+status_code = 404
+__init__(detail?, headers?)
}
class AuthException {
+int status_code
+str detail
+dict headers
}
class AuthenticationFailed {
+status_code = 401
+detail = "Authentication failed"
}
class PermissionDenied {
+status_code = 403
+detail = "Permission denied"
}
class RequestValidationError {
+list errors
+Any body
}
class ResponseValidationError {
+list errors
+Any body
}
class WebSocketException {
+int code
+str reason
+__str__() str
+__repr__() str
}
Exception <|-- HTTPException
Exception <|-- RequestValidationError
Exception <|-- ResponseValidationError
Exception <|-- WebSocketException
HTTPException <|-- NotFoundException
HTTPException <|-- AuthException
AuthException <|-- AuthenticationFailed
AuthException <|-- PermissionDenied
2.1 HTTPException
Section titled “2.1 HTTPException”File: core/sillo/exceptions.py (lines 15 to 115)
The root of all HTTP error exceptions. Every handler that wants to produce a non-2xx response raises this (or a subclass).
class HTTPException(Exception): def __init__( self, status_code: int, detail: typing.Any | None = None, headers: dict[str, typing.Any] = {}, ) -> None: super().__init__(detail or http.HTTPStatus(status_code).phrase) self.status_code = status_code self.detail = self.args[0] self.headers = headersKey behaviors:
- If no
detailis provided, the standard HTTP reason phrase fromhttp.HTTPStatusis used (e.g., 404 →"Not Found"). - The
detailis stored both inself.args[0](viasuper().__init__) and inself.detail. This makes it work withstr(exc)andexc.detailalike. - The
headersdict defaults to{}(mutable default: intentional, only read, never mutated by the framework). __str__returns"HTTP {status_code}: {detail}".__repr__returns"HTTPException(404, 'Not Found')"(uses the actual class name, so subclasses get the right name).
Raising patterns
Section titled “Raising patterns”# Explicit status + detailraise HTTPException(status_code=400, detail="Bad request body")
# Status-only (detail auto-derived from HTTPStatus)raise HTTPException(status_code=429)
# With custom headers (e.g., WWW-Authenticate on 401)raise HTTPException( status_code=401, detail="Token expired", headers={"WWW-Authenticate": "Bearer"},)2.2 NotFoundException
Section titled “2.2 NotFoundException”File: core/sillo/exceptions.py (lines 118 to 159)
A convenience subclass that hardcodes status_code=404:
class NotFoundException(HTTPException): def __init__( self, detail: str | None = None, headers: dict[str, typing.Any] = {}, ) -> None: super().__init__( status_code=404, detail=detail or "Not Found", headers=headers, )Why a separate class? Two reasons:
- The
ExceptionMiddlewareregisters a class-based handler forNotFoundException→handle_404_error, which provides content negotiation (HTML for browsers, JSON for APIs). If you raise a bareHTTPException(404), you get the generic JSON handler instead. - It makes intent explicit in code:
raise NotFoundException()reads better thanraise HTTPException(404).
2.3 WebSocketException
Section titled “2.3 WebSocketException”File: core/sillo/exceptions.py (lines 162 to 242)
A completely separate exception hierarchy (does NOT inherit from
HTTPException) because WebSocket connections use close codes, not HTTP status
codes:
class WebSocketException(Exception): def __init__(self, code: int, reason: str | None = None) -> None: super().__init__(reason or "") self.code = code self.reason = self.args[0]codeis a WebSocket close code (RFC 6455): 1000 (normal), 1008 (policy violation), 1011 (internal error), etc.reasonis an optional human-readable string (limited to 123 bytes by the WebSocket protocol).__str__→"WebSocket {code}: {reason}".__repr__→"WebSocketException(1008, 'Policy violation')".
WebSocket exceptions are caught by a completely separate middleware
(WebSocketErrorMiddleware in core/sillo/websockets/errors.py), not by
ExceptionMiddleware.
2.4 AuthException / AuthenticationFailed / PermissionDenied
Section titled “2.4 AuthException / AuthenticationFailed / PermissionDenied”File: core/sillo/auth/exceptions.py (lines 16 to 161)
class AuthException(HTTPException): def __init__( self, status_code: int, detail: str, headers: HeadersType | None = None ) -> None: super().__init__(status_code, detail, headers or {})
class AuthenticationFailed(AuthException): def __init__( self, detail: str = "Authentication failed", headers: HeadersType | None = None, ) -> None: super().__init__(401, detail, headers)
class PermissionDenied(AuthException): def __init__( self, detail: str = "Permission denied", headers: HeadersType | None = None, ) -> None: super().__init__(403, detail, headers)The inheritance chain is: AuthenticationFailed → AuthException →
HTTPException → Exception.
This means:
- Catching
HTTPExceptioncatches auth errors too (they produce HTTP responses). - Catching
AuthExceptioncatches bothAuthenticationFailedandPermissionDenied. - The
ExceptionMiddlewareregistersAuthenticationFailed→AuthErrorHandlerat the class level, so auth failures get a dedicated JSON response handler.
2.5 RequestValidationError / ResponseValidationError
Section titled “2.5 RequestValidationError / ResponseValidationError”File: core/sillo/validation/errors.py (lines 68 to 126)
These inherit directly from Exception (NOT from HTTPException). They carry
structured error lists rather than a single status code:
class RequestValidationError(Exception): def __init__(self, errors: list[dict[str, Any]], *, body: Any = None) -> None: self.errors = errors self.body = body super().__init__(f"{len(errors)} validation error(s) in request")
class ResponseValidationError(Exception): def __init__(self, errors: list[dict[str, Any]], *, body: Any = None) -> None: self.errors = errors self.body = body super().__init__(f"{len(errors)} validation error(s) in response")Why not inherit from HTTPException? Because ResponseValidationError maps
to HTTP 500 (server error, not client error), while RequestValidationError
maps to 422. If they inherited from HTTPException, the status-code-based
dispatch would kick in and could conflict with the class-based handlers. By
being plain Exception subclasses, they always go through the class-based MRO
lookup.
3. ExceptionMiddleware: The Core Pipeline
Section titled “3. ExceptionMiddleware: The Core Pipeline”File: core/sillo/exception_handler.py (lines 129 to 261)
ExceptionMiddleware is a standard sillo middleware that wraps request
processing in a try/except. It is not an ASGI middleware directly. It
conforms to the sillo middleware signature:
async def __call__( self, request: Request, response: Response, call_next: Callable[[], Awaitable[Response]],) -> ResponseInitialization
Section titled “Initialization”class ExceptionMiddleware: def __init__(self) -> None: self.debug = False self._status_handlers: dict[int, ExceptionHandlerType] = {} self._exception_handlers = { HTTPException: self.http_exception, AuthenticationFailed: AuthErrorHandler, NotFoundException: handle_404_error, ValidationError: pydantic_validation_error_handler, RequestValidationError: request_validation_error_handler, ResponseValidationError: response_validation_error_handler, }On construction, the middleware creates:
- An empty
_status_handlersdict (integer keys → handler callables). - A pre-populated
_exception_handlersdict (class keys → handler callables) with six default entries.
The __call__ Path
Section titled “The __call__ Path”async def __call__(self, request, response, call_next): if len(self._exception_handlers) == 0 and len(self._status_handlers) == 0: return await call_next() # fast path: no handlers at all return await wrap_http_exceptions( request=request, response=response, call_next=call_next, exception_handlers=self._exception_handlers, status_handlers=self._status_handlers, )The fast-path optimization skips the try/except wrapper when both registries
are empty. In practice this never fires because the constructor pre-populates
_exception_handlers.
4. The Two Registries
Section titled “4. The Two Registries”The ExceptionMiddleware maintains two separate dictionaries:
4.1 _status_handlers: dict[int, ExceptionHandlerType]
Section titled “4.1 _status_handlers: dict[int, ExceptionHandlerType]”- Keys: Integer HTTP status codes (e.g.,
404,500,429). - Values: Async handler callables with signature
(request, response, exc) -> Response. - Lookup: Only checked when the exception is an
HTTPExceptioninstance. The lookup usesexc.status_codeas the key. - Priority: Checked first, before class-based handlers. If a status-code handler is found, the class-based handler is never consulted.
- Default: Empty on init. Applications populate it via
add_exception_handler(int, handler).
# Register a custom handler for 429 Too Many Requestsapp.add_exception_handler(429, rate_limit_handler)4.2 _exception_handlers: dict[type[Exception], ExceptionHandlerType]
Section titled “4.2 _exception_handlers: dict[type[Exception], ExceptionHandlerType]”- Keys: Exception classes (not instances).
- Values: Async handler callables.
- Lookup: Uses MRO traversal (
_lookup_exception_handler). - Priority: Checked second, only if no status-code handler matched (or the
exception is not an
HTTPException). - Default: Pre-populated with six entries (see §3 above).
# Register a custom handler for a domain exceptionapp.add_exception_handler(InsufficientCreditsError, credits_error_handler)Priority Relationship
Section titled “Priority Relationship”flowchart TD
A["Exception raised"] --> B{"Is it an HTTPException?"}
B -- Yes --> C{"Status code in\n_status_handlers?"}
C -- Yes --> D["Execute status handler\n(PRIORITY 1)"]
C -- No --> E{"Class in\n_exception_handlers\nor MRO match?"}
B -- No --> E
E -- Found --> F["Execute class handler\n(PRIORITY 2)"]
E -- Not found --> G["Log traceback\nRe-raise exception"]
D --> H["Return Response"]
F --> H
This two-tier design means:
- You can override the handler for all 404 responses by registering a
status-code handler:
app.add_exception_handler(404, my_handler). This takes precedence over the class-basedNotFoundExceptionhandler. - You can register a handler for a specific exception class (e.g.,
RateLimitExceeded) without touching status codes. - The status-code registry is O(1) lookup; the class registry is O(n) where n is the MRO depth (typically ≤ 5).
5. Handler Lookup: MRO-Based Resolution
Section titled “5. Handler Lookup: MRO-Based Resolution”File: core/sillo/exception_handler.py (lines 28 to 60)
def _lookup_exception_handler( exc_handlers: dict[int | type[Exception], ExceptionHandlerType], exc: Exception,): for cls in type(exc).__mro__: if cls in exc_handlers: return exc_handlers[cls] return NoneThe algorithm walks the exception’s Method Resolution Order (MRO), which for a typical Python class looks like:
>>> type(exc).__mro__(AuthenticationFailed, AuthException, HTTPException, Exception, object)At each step, it checks if that class is a key in exc_handlers. The first
match wins, which gives the most specific handler.
Walkthrough Example
Section titled “Walkthrough Example”Suppose the registry contains:
{ HTTPException: http_exception_handler, AuthenticationFailed: auth_handler, Exception: fallback_handler,}And an AuthenticationFailed is raised. The MRO walk is:
| Step | Class in MRO | In registry? | Action |
|---|---|---|---|
| 1 | AuthenticationFailed | ✅ Yes | Return auth_handler ← match |
| 2 | AuthException | (not reached) | |
| 3 | HTTPException | (not reached) | |
| 4 | Exception | (not reached) | |
| 5 | object | (not reached) |
If the exception were a bare HTTPException(403) (not an AuthException
subclass):
| Step | Class in MRO | In registry? | Action |
|---|---|---|---|
| 1 | HTTPException | ✅ Yes | Return http_exception_handler ← match |
If the exception were a ValueError (nothing in the registry matches):
| Step | Class in MRO | In registry? | Action |
|---|---|---|---|
| 1 | ValueError | ❌ No | Continue |
| 2 | Exception | ✅ Yes | Return fallback_handler ← match |
| 3 | object | (not reached) |
If even Exception is not in the registry, the walk reaches object, finds
nothing, and returns None. The caller logs the traceback and re-raises.
MRO Diagram
Section titled “MRO Diagram”flowchart TD
START["exc = AuthenticationFailed()"] --> MRO["type(exc).__mro__\n= [AuthenticationFailed, AuthException,\n HTTPException, Exception, object]"]
MRO --> S1{"AuthenticationFailed\nin registry?"}
S1 -- Yes --> R1["✅ Return handler"]
S1 -- No --> S2{"AuthException\nin registry?"}
S2 -- Yes --> R2["✅ Return handler"]
S2 -- No --> S3{"HTTPException\nin registry?"}
S3 -- Yes --> R3["✅ Return handler"]
S3 -- No --> S4{"Exception\nin registry?"}
S4 -- Yes --> R4["✅ Return handler"]
S4 -- No --> S5{"object\nin registry?"}
S5 -- Yes --> R5["✅ Return handler"]
S5 -- No --> NONE["❌ Return None\n→ log + re-raise"]
6. The wrap_http_exceptions Dispatch Function
Section titled “6. The wrap_http_exceptions Dispatch Function”File: core/sillo/exception_handler.py (lines 63 to 126)
This is the heart of the exception pipeline. It is called by
ExceptionMiddleware.__call__ and contains the full try/except dispatch logic:
async def wrap_http_exceptions( request: Request, response: Response, call_next: Callable[..., Awaitable[Response]], exception_handlers: dict[int | type[Exception], ExceptionHandlerType], status_handlers: dict[int, ExceptionHandlerType],): exception_handlers = exception_handlers or {} status_handlers = status_handlers or {}
try: return await call_next() except Exception as exc: handler: ExceptionHandlerType | None = None
# Step 1: Status-code lookup (HTTPException only) if isinstance(exc, HTTPException): handler = status_handlers.get(exc.status_code) if handler: return await handler(request, response, exc)
# Step 2: Class-based MRO lookup if handler is None: handler = _lookup_exception_handler(exception_handlers, exc) if not handler: error = traceback.format_exc() logger.error(error) raise return await handler(request, response, exc)Step-by-step logic:
Section titled “Step-by-step logic:”- Execute
call_next(): runs the next middleware or route handler. - If an exception is raised:
a. If it’s an
HTTPException, look upexc.status_codeinstatus_handlers. If found, call that handler immediately. Return. b. If no status handler matched (or the exception is not anHTTPException), perform MRO-based lookup inexception_handlers. c. If a class handler is found, call it and return. d. If nothing matches, log the full traceback and re-raise. The server error middleware (ServerErrorMiddleware) catches it and produces a 500.
Important subtlety
Section titled “Important subtlety”The variable handler is initialized to None and is used as a sentinel. The
if handler is None check on line 120 is reached in two cases:
- The exception is not an
HTTPException(so theisinstancecheck on line 115 was False, andhandlerwas never assigned). - The exception IS an
HTTPExceptionbut no status-code handler was found (status_handlers.get(exc.status_code)returned None).
In both cases, the class-based lookup runs. This means:
- An
HTTPException(404)with no status-code handler will match the class-basedHTTPExceptionhandler (the defaulthttp_exceptionmethod). - A
NotFoundException(which IS anHTTPException) with no status-code handler will matchNotFoundExceptionfirst in the MRO walk (more specific), falling back toHTTPExceptionif needed.
7. Built-in Default Handlers
Section titled “7. Built-in Default Handlers”7.1 HTTPException → http_exception (JSON or empty)
Section titled “7.1 HTTPException → http_exception (JSON or empty)”File: core/sillo/exception_handler.py (lines 263 to 296)
async def http_exception( self, request: Request, response: Response, exc: HTTPException) -> Response: assert isinstance(exc, HTTPException) if exc.status_code in {204, 304}: return response.empty(status_code=exc.status_code, headers=exc.headers) return response.json( exc.detail, status_code=exc.status_code, headers=exc.headers )Behavior:
| Status | Response |
|---|---|
| 204, 304 | Empty body, appropriate status code, includes custom headers |
| Everything else | response.json(exc.detail, status_code=..., headers=...) |
The 204/304 special case exists because HTTP specifications prohibit response
bodies for these status codes. The handler returns response.empty() which
produces a response with no body.
Response shape (non-204/304):
"detail message here"Note: the response body is exc.detail directly (a string or any
JSON-serializable value), not wrapped in a {"detail": ...} envelope. This
differs from the 404 handler (§7.3) and the validation handlers (§7.4 to 7.6).
7.2 AuthenticationFailed → AuthErrorHandler
Section titled “7.2 AuthenticationFailed → AuthErrorHandler”File: core/sillo/auth/exceptions.py (lines 164 to 197)
async def AuthErrorHandler( request: Request, response: Response, exc: HTTPException) -> Any: return response.json(exc.detail, status_code=exc.status_code, headers=exc.headers)This handler is registered for the AuthenticationFailed class. Because of MRO
lookup, it also catches any subclass of AuthenticationFailed (though there
are none currently).
Response shape:
"Authentication failed"Same as the generic http_exception handler. The body is exc.detail
directly. The handler exists as a separate entry point so applications can
override it independently of the generic HTTPException handler.
7.3 NotFoundException → handle_404_error (Content-Negotiated 404)
Section titled “7.3 NotFoundException → handle_404_error (Content-Negotiated 404)”File: core/sillo/handlers/not_found.py (lines 62 to 118)
This is the most sophisticated built-in handler. It performs content negotiation
based on the client’s Accept header:
async def handle_404_error( request: Request, response: Response, exception: NotFoundException,) -> Response: debug = _debug_enabled(request)
if debug: error_message = exception.detail traceback_info = traceback.format_exc() if traceback_info.strip() == "NoneType: None": traceback_info = None else: error_message = GENERIC_MESSAGE # "The page you are looking for does not exist." traceback_info = None
if _prefers_html(request): return response.html( generate_html_page("404 - Not Found", error_message), status_code=404, )
if request.accepts_json: error_details = { "status": 404, "error": http.HTTPStatus(404).phrase, "message": error_message, } if traceback_info: error_details["traceback"] = traceback_info return response.json(error_details, status_code=404)
return response.text( f"404 - Not Found\n{error_message}", status_code=404, )Content negotiation logic:
flowchart TD
REQ["NotFoundException raised"] --> DBG{"debug enabled?"}
DBG -- Yes --> DET["error_message = exception.detail\ntraceback = formatted"]
DBG -- No --> GEN["error_message = GENERIC_MESSAGE\ntraceback = None"]
DET --> HTML{"Accept header\ncontains text/html?"}
GEN --> HTML
HTML -- Yes --> HTMLR["response.html(HTML page, 404)"]
HTML -- No --> JSON{"Accept header\ncontains application/json\nor accepts_json?"}
JSON -- Yes --> JSONR["response.json({...}, 404)\n+ optional traceback"]
JSON -- No --> TEXTR["response.text('404 - Not Found\\n...', 404)"]
Debug flag resolution (_debug_enabled, line 121 to 144):
def _debug_enabled(request: Request) -> bool: scope = getattr(request, "scope", {}) or {} for key in ("base_app", "app"): candidate = scope.get(key) if candidate is not None and hasattr(candidate, "debug"): return bool(candidate.debug) return FalseThe handler looks for base_app first, then app in the ASGI scope. This is
because app holds the router (which has no debug flag), while base_app
holds the SilloApp instance. If neither has a debug attribute, the default is
False (production-safe).
HTML preference check (_prefers_html, line 147 to 162):
def _prefers_html(request: Request) -> bool: accept = request.headers.get("accept", "") return "text/html" in accept or "application/xhtml+xml" in acceptA wildcard */* is NOT treated as a preference for HTML, only explicit
text/html or application/xhtml+xml triggers the HTML page.
7.4 ValidationError → pydantic_validation_error_handler (422)
Section titled “7.4 ValidationError → pydantic_validation_error_handler (422)”File: core/sillo/exception_handler.py (lines 359 to 407)
Handles raw Pydantic ValidationError instances (from model construction,
not from request validation):
async def pydantic_validation_error_handler( request: Request, response: Response, exc: ValidationError) -> Response: errors = exc.errors() error_dict = {} for e in errors: loc, msg = e["loc"], e["msg"] if len(loc) == 1: error_dict[loc[0]] = msg elif len(loc) == 2: nested = error_dict.get(loc[0]) if not isinstance(nested, dict): nested = {} error_dict[loc[0]] = nested nested[loc[1]] = msg else: error_dict[".".join(map(str, loc))] = msg return response.json( {"error": "Validation Error", "errors": error_dict}, status_code=422, )Response shape:
{ "error": "Validation Error", "errors": { "name": "field required", "address": { "city": "field required" }, "items.0.price": "ensure this value is greater than 0" }}The nesting strategy:
- 1-level path: flat key (
"name": "...") - 2-level path: nested dict (
"address": {"city": "..."}) - 3+ level path: dot-joined key (
"items.0.price": "...")
7.5 RequestValidationError → request_validation_error_handler (422)
Section titled “7.5 RequestValidationError → request_validation_error_handler (422)”File: core/sillo/exception_handler.py (lines 299 to 323)
async def request_validation_error_handler( request: Request, response: Response, exc: RequestValidationError) -> Response: return response.json({"detail": exc.errors}, status_code=422)Response shape:
{ "detail": [ { "loc": ["query", "page"], "msg": "value is not a valid integer", "type": "type_error.integer" }, { "loc": ["body", "name"], "msg": "field required", "type": "value_error.missing" } ]}The key difference from the Pydantic handler: RequestValidationError already
carries location-prefixed errors (the prefix_errors function in
core/sillo/validation/errors.py adds the location prefix). The handler
serializes them directly.
7.6 ResponseValidationError → response_validation_error_handler (500)
Section titled “7.6 ResponseValidationError → response_validation_error_handler (500)”File: core/sillo/exception_handler.py (lines 326 to 356)
async def response_validation_error_handler( request: Request, response: Response, exc: ResponseValidationError) -> Response: logger.error( "Response validation failed for %s %s: %s", request.method, request.url.path, exc.errors, ) return response.json( {"error": "Internal Server Error", "detail": "Response validation failed"}, status_code=500, )Response shape:
{ "error": "Internal Server Error", "detail": "Response validation failed"}Critical design decision: The response does NOT include exc.errors or
exc.body. This is intentional. The handler’s return value violated the
response model, which means it may contain data that the response model was
supposed to filter out. Echoing it back could leak internal state.
The error details ARE logged server-side for debugging.
7.7 Summary of Default Handler Responses
Section titled “7.7 Summary of Default Handler Responses”| Exception Class | Handler | HTTP Status | Response Body |
|---|---|---|---|
HTTPException | http_exception | exc.status_code | exc.detail (raw JSON) |
HTTPException (204/304) | http_exception | 204/304 | Empty body |
AuthenticationFailed | AuthErrorHandler | 401 | exc.detail (raw JSON) |
NotFoundException | handle_404_error | 404 | HTML, JSON, or plain text (content-negotiated) |
ValidationError (Pydantic) | pydantic_validation_error_handler | 422 | {"error": "...", "errors": {...}} |
RequestValidationError | request_validation_error_handler | 422 | {"detail": [...]} |
ResponseValidationError | response_validation_error_handler | 500 | {"error": "...", "detail": "..."} |
8. Database Exception Handlers
Section titled “8. Database Exception Handlers”File: core/sillo/record/exceptions.py (lines 1 to 110)
Sillo provides built-in handlers for Tortoise ORM exceptions. These are NOT
registered by default. Applications must call
register_db_exception_handlers(app):
def register_db_exception_handlers(app) -> None: app.add_exception_handler(DoesNotExist, handle_does_not_exist) app.add_exception_handler(IntegrityError, handle_integrity_error) app.add_exception_handler(ValidationError, handle_validation_error) app.add_exception_handler(OperationalError, handle_operational_error)8.1 DoesNotExist → 404
Section titled “8.1 DoesNotExist → 404”async def handle_does_not_exist(request, response, exc: DoesNotExist): return response.json( {"error": "Not Found", "detail": str(exc)}, status_code=404, )Triggered when a Tortoise .get() query finds no matching record.
8.2 IntegrityError → 409
Section titled “8.2 IntegrityError → 409”async def handle_integrity_error(request, response, exc: IntegrityError): return response.json( {"error": "Conflict", "detail": str(exc)}, status_code=409, )Triggered on unique constraint violations, foreign key violations, or null constraint violations at the database level.
8.3 ValidationError → 422
Section titled “8.3 ValidationError → 422”async def handle_validation_error(request, response, exc: ValidationError): return response.json( {"error": "Validation Error", "detail": str(exc)}, status_code=422, )Triggered by Tortoise model-level validation (type mismatches, out-of-range
values, etc.). Note: this is tortoise.exceptions.ValidationError, NOT
pydantic.ValidationError.
8.4 OperationalError → 503
Section titled “8.4 OperationalError → 503”async def handle_operational_error(request, response, exc: OperationalError): return response.json( {"error": "Service Unavailable", "detail": "Database unavailable"}, status_code=503, )Triggered when the database is unreachable (connection refused, timeout, network partition). The detail message is deliberately generic to avoid leaking infrastructure details.
DB Handler Registration Flow
Section titled “DB Handler Registration Flow”sequenceDiagram
participant App as SilloApp
participant MW as ExceptionMiddleware
participant DB as record/exceptions.py
App->>DB: register_db_exception_handlers(app)
DB->>MW: add_exception_handler(DoesNotExist, handle_does_not_exist)
DB->>MW: add_exception_handler(IntegrityError, handle_integrity_error)
DB->>MW: add_exception_handler(ValidationError, handle_validation_error)
DB->>MW: add_exception_handler(OperationalError, handle_operational_error)
Note over MW: _exception_handlers now has<br/>4 additional class-based entries
Collision Warning: Tortoise ValidationError vs Pydantic ValidationError
Section titled “Collision Warning: Tortoise ValidationError vs Pydantic ValidationError”The default ExceptionMiddleware registers a handler for
pydantic.ValidationError. When register_db_exception_handlers is called, it
adds a handler for tortoise.exceptions.ValidationError. These are different
classes. There is no collision. The MRO lookup correctly resolves to the
right handler based on the exception’s actual type.
However, if you register a handler for the base Exception class, it will
catch BOTH validation error types (and everything else). Be specific.
9. WebSocket Exception Handling
Section titled “9. WebSocket Exception Handling”File: core/sillo/websockets/errors.py (lines 1 to 40)
WebSocket connections have a completely separate exception pipeline.
ExceptionMiddleware does NOT handle WebSocket exceptions. They are caught by
WebSocketErrorMiddleware:
class WebSocketErrorMiddleware: def __init__(self, app: ASGIApp): self.app = app
async def __call__(self, scope: Scope, receive: Receive, send: Send): if scope["type"] == "websocket": websocket = WebSocket(scope, receive, send) try: await self.app(scope, receive, send) except WebSocketException as exc: await websocket_exception_handler(websocket, exc) except Exception: error = traceback.format_exc() logger.error(f"Unexpected error: {error}") await websocket.close(code=1011, reason="Internal Server Error") else: await self.app(scope, receive, send)Two catch branches:
WebSocketException→websocket_exception_handler→ sends a close frame with the exception’scodeandreason.- Any other
Exception→ logs the traceback and sends a close frame with code 1011 (Internal Server Error) and a generic reason.
async def websocket_exception_handler( websocket: WebSocket, exc: WebSocketException) -> None: error = traceback.format_exc() logger.error(f"WebSocket error: {error}") await websocket.close(code=exc.code, reason=str(exc))WebSocket Exception Flow
Section titled “WebSocket Exception Flow”flowchart TD
WS["WebSocket connection"] --> MW["WebSocketErrorMiddleware"]
MW --> TRY["await self.app(scope, receive, send)"]
TRY --> OK["Success → normal WS lifecycle"]
TRY --> WSE{"WebSocketException?"}
WSE -- Yes --> HANDLER["websocket_exception_handler"]
HANDLER --> CLOSE1["websocket.close(code=exc.code, reason=str(exc))"]
WSE -- No --> OTHER{"Any other Exception?"}
OTHER -- Yes --> LOG["logger.error(traceback)"]
LOG --> CLOSE2["websocket.close(code=1011, reason='Internal Server Error')"]
OTHER -- No --> OK
Common WebSocket Close Codes
Section titled “Common WebSocket Close Codes”| Code | Name | When to use |
|---|---|---|
| 1000 | Normal Closure | Connection completed successfully |
| 1001 | Going Away | Server shutting down, client navigating away |
| 1008 | Policy Violation | Authentication failure, rate limiting |
| 1011 | Internal Server Error | Unexpected server-side error |
| 1013 | Try Again Later | Server temporarily overloaded |
# Raise from a WebSocket handlerraise WebSocketException(code=1008, reason="Authentication required")
# Close with no reason (defaults to empty string)raise WebSocketException(code=1000)10. Content Negotiation in Error Responses
Section titled “10. Content Negotiation in Error Responses”The handle_404_error handler is the only built-in handler that performs
content negotiation. All other handlers return JSON unconditionally.
Negotiation Algorithm
Section titled “Negotiation Algorithm”- Read the
Acceptheader from the request. - If it contains
text/htmlorapplication/xhtml+xml→ return an HTML page. - If
request.accepts_jsonis True → return JSON. - Otherwise → return plain text.
HTML Error Page Generation
Section titled “HTML Error Page Generation”File: core/sillo/handlers/not_found.py (lines 12 to 59)
def generate_html_page(title: str, message: str) -> str: return f"""<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>{title}</title> <style> body {{ font-family: Arial, sans-serif; text-align: center; margin: 50px; color: #333; }} h1 {{ font-size: 48px; color: #d9534f; }} p {{ font-size: 18px; margin-top: 10px; }} </style></head><body> <h1>{title}</h1> <p>{message}</p></body></html>"""The page is self-contained (no external CSS/JS dependencies) and uses a minimal, centered layout.
Content Type Decision Matrix
Section titled “Content Type Decision Matrix”| Accept Header | Response Format | Status |
|---|---|---|
text/html | HTML page | 404 |
application/xhtml+xml | HTML page | 404 |
application/json | JSON object | 404 |
*/* | JSON object | 404 |
| (empty/missing) | JSON object | 404 |
text/plain | Plain text | 404 |
image/png | Plain text | 404 |
11. Validation Error Architecture
Section titled “11. Validation Error Architecture”11.1 Location-Prefixing: prefix_errors
Section titled “11.1 Location-Prefixing: prefix_errors”File: core/sillo/validation/errors.py (lines 15 to 65)
The prefix_errors function converts Pydantic ValidationError instances into
location-prefixed error dictionaries:
def prefix_errors( exc: ValidationError, location: str, *, alias_map: dict[str, str] | None = None,) -> list[dict[str, Any]]: out: list[dict[str, Any]] = [] for err in exc.errors(): loc: Sequence[Any] = err.get("loc", ()) first = loc[0] if loc else None if alias_map and isinstance(first, str) and first in alias_map: loc = (alias_map[first], *loc[1:]) item = { "loc": [location, *loc], "msg": err.get("msg", ""), "type": err.get("type", ""), } if "input" in err: item["input"] = err["input"] out.append(item) return outExample transformation:
Pydantic reports: {"loc": ("page",), "msg": "value is not a valid integer", ...}
After prefix_errors(exc, "query"): {"loc": ["query", "page"], "msg": "...", ...}
The alias_map parameter handles the case where a Python field name differs
from the wire name (e.g., due to Pydantic aliases).
11.2 Two Validation Error Types
Section titled “11.2 Two Validation Error Types”| Exception | Source | HTTP Status | When raised |
|---|---|---|---|
RequestValidationError | Client input | 422 | Request data fails validation markers (Query, Path, Body, etc.) |
ResponseValidationError | Server output | 500 | Handler return value violates response_model |
Key distinction: RequestValidationError is a client error (the client sent
bad data). ResponseValidationError is a server error (the application
produced bad output). They map to different HTTP status codes to make this
distinction clear to API clients.
11.3 Error Accumulation
Section titled “11.3 Error Accumulation”RequestValidationError accumulates errors across all request locations in a
single exception. A single request can have bad query parameters AND a malformed
body, producing a single 422 response with all errors listed:
{ "detail": [ {"loc": ["query", "page"], "msg": "value is not a valid integer", "type": "type_error.integer"}, {"loc": ["body", "name"], "msg": "field required", "type": "value_error.missing"}, {"loc": ["path", "team_id"], "msg": "value is not a valid integer", "type": "type_error.integer"} ]}This avoids the N+1 round-trip problem where clients fix one error per request.
12. Handler Signature Contract
Section titled “12. Handler Signature Contract”All exception handlers must conform to the ExceptionHandlerType defined in
core/sillo/types.py (line 39):
ExceptionHandlerType = Callable[[Request, Response, Exception], Response]In practice, handlers are async callables with this signature:
async def my_handler( request: Request, response: Response, exc: SomeException,) -> Response: ...Parameters:
| Parameter | Type | Description |
|---|---|---|
request | Request | The incoming HTTP request. Provides access to headers, URL, method, scope, and the application instance. |
response | Response (Responder) | A response factory. Call .json(), .html(), .text(), or .empty() to produce the HTTP response. |
exc | Exception subclass | The caught exception. Handlers should type-hint this to the specific exception class they handle. |
Return: A Response object. The middleware sends this back to the client.
Response Factory Methods
Section titled “Response Factory Methods”| Method | Use case |
|---|---|
response.json(data, status_code=..., headers=...) | JSON responses (most common) |
response.html(html_string, status_code=...) | HTML responses (404 page) |
response.text(text, status_code=...) | Plain text responses |
response.empty(status_code=..., headers=...) | Empty body (204, 304) |
13. Registration Patterns
Section titled “13. Registration Patterns”13.1 Via add_exception_handler
Section titled “13.1 Via add_exception_handler”# Status-code based (goes into _status_handlers)app.add_exception_handler(429, my_rate_limit_handler)
# Class-based (goes into _exception_handlers)app.add_exception_handler(InsufficientCreditsError, my_credits_handler)13.2 Via register_db_exception_handlers
Section titled “13.2 Via register_db_exception_handlers”from sillo.record.exceptions import register_db_exception_handlers
app = SilloApp()register_db_exception_handlers(app)13.3 Overriding a Default Handler
Section titled “13.3 Overriding a Default Handler”# Override the default 404 handlerasync def custom_404(request, response, exc): return response.json({"error": "custom not found"}, status_code=404)
app.add_exception_handler(NotFoundException, custom_404)13.4 Catch-All Handler
Section titled “13.4 Catch-All Handler”# Catch any unhandled exception (risky — use with caution)async def catch_all(request, response, exc): logger.exception("Unhandled exception") return response.json({"error": "Internal Server Error"}, status_code=500)
app.add_exception_handler(Exception, catch_all)13.5 Status-Code Override vs Class Override
Section titled “13.5 Status-Code Override vs Class Override”# This catches ALL 404 responses (including NotFoundException)app.add_exception_handler(404, my_404_handler)
# This only catches NotFoundException specificallyapp.add_exception_handler(NotFoundException, my_404_handler)The status-code handler takes priority (checked first in wrap_http_exceptions).
14. Pipeline Flow Diagrams
Section titled “14. Pipeline Flow Diagrams”14.1 Complete Exception Handling Pipeline
Section titled “14.1 Complete Exception Handling Pipeline”flowchart TD
CLIENT["Client Request"] --> BRIDGE["ASGIRequestResponseBridge"]
BRIDGE --> MW_CHAIN["Middleware Chain"]
MW_CHAIN --> EM["ExceptionMiddleware.__call__"]
EM --> FAST{"Both registries\nempty?"}
FAST -- Yes --> NEXT["call_next() → Route Handler"]
FAST -- No --> WRAP["wrap_http_exceptions()"]
WRAP --> TRY["try: await call_next()"]
TRY --> SUCCESS["✅ Return Response"]
TRY --> EXC["❌ Exception raised"]
EXC --> IS_HTTP{"isinstance(exc,\nHTTPException)?"}
IS_HTTP -- Yes --> STATUS{"exc.status_code\nin _status_handlers?"}
STATUS -- Yes --> SH["Execute status handler"]
SH --> SRESP["Return Response"]
STATUS -- No --> MRO_HTTP["_lookup_exception_handler\n(exception_handlers, exc)"]
IS_HTTP -- No --> MRO_OTHER["_lookup_exception_handler\n(exception_handlers, exc)"]
MRO_HTTP --> FOUND_HTTP{"Handler found?"}
MRO_OTHER --> FOUND_OTHER{"Handler found?"}
FOUND_HTTP -- Yes --> EXEC_HTTP["Execute handler"]
EXEC_HTTP --> HRESP["Return Response"]
FOUND_OTHER -- Yes --> EXEC_OTHER["Execute handler"]
EXEC_OTHER --> HRESP2["Return Response"]
FOUND_HTTP -- No --> LOG_HTTP["logger.error(traceback)\nRe-raise"]
FOUND_OTHER -- No --> LOG_OTHER["logger.error(traceback)\nRe-raise"]
LOG_HTTP --> SERVE["ServerErrorMiddleware\n→ 500 response"]
LOG_OTHER --> SERVE
NEXT --> ROUTE_OK["Handler returns Response"]
ROUTE_OK --> SUCCESS
style EM fill:#f9f,stroke:#333
style WRAP fill:#f9f,stroke:#333
style SERVE fill:#fbb,stroke:#333
14.2 MRO Lookup Detail
Section titled “14.2 MRO Lookup Detail”flowchart TD
INPUT["_lookup_exception_handler(exc_handlers, exc)"] --> MRO["Get type(exc).__mro__"]
MRO --> LOOP["For each cls in MRO:"]
LOOP --> CHECK{"cls in exc_handlers?"}
CHECK -- Yes --> RETURN["return exc_handlers[cls]"]
CHECK -- No --> NEXT_CLS{"More classes\nin MRO?"}
NEXT_CLS -- Yes --> LOOP
NEXT_CLS -- No --> NONE["return None"]
14.3 Default Handler Registration Map
Section titled “14.3 Default Handler Registration Map”flowchart LR
subgraph ExceptionMiddleware
subgraph _status_handlers ["_status_handlers (int keys)"]
SH_EMPTY["(empty by default)"]
end
subgraph _exception_handlers ["_exception_handlers (class keys)"]
EH1["HTTPException → http_exception"]
EH2["AuthenticationFailed → AuthErrorHandler"]
EH3["NotFoundException → handle_404_error"]
EH4["ValidationError → pydantic_validation_error_handler"]
EH5["RequestValidationError → request_validation_error_handler"]
EH6["ResponseValidationError → response_validation_error_handler"]
end
end
14.4 Database Handler Registration Map
Section titled “14.4 Database Handler Registration Map”flowchart LR
subgraph After register_db_exception_handlers
subgraph _exception_handlers ["_exception_handlers (class keys)"]
EH1["HTTPException → http_exception"]
EH2["AuthenticationFailed → AuthErrorHandler"]
EH3["NotFoundException → handle_404_error"]
EH4["ValidationError → pydantic_validation_error_handler"]
EH5["RequestValidationError → request_validation_error_handler"]
EH6["ResponseValidationError → response_validation_error_handler"]
DB1["DoesNotExist → handle_does_not_exist"]
DB2["IntegrityError → handle_integrity_error"]
DB3["Tortoise ValidationError → handle_validation_error"]
DB4["OperationalError → handle_operational_error"]
end
end
15. Edge Cases and Gotchas
Section titled “15. Edge Cases and Gotchas”15.1 Mutable Default Headers
Section titled “15.1 Mutable Default Headers”HTTPException.__init__ and NotFoundException.__init__ use mutable default
arguments (headers: dict = {}). This is a known Python anti-pattern, but the
framework only reads the dict, never mutates it. If you need to pass headers,
always pass an explicit dict:
# Goodraise HTTPException(401, headers={"WWW-Authenticate": "Bearer"})
# Technically works but risky if anyone mutates the defaultraise HTTPException(401)15.2 Status-Code Handler Precedence
Section titled “15.2 Status-Code Handler Precedence”Status-code handlers are checked BEFORE class-based handlers. This means:
# This handler fires for ALL 404s, even NotFoundExceptionapp.add_exception_handler(404, my_handler)
# This handler is IGNORED when the status-code handler matchesapp.add_exception_handler(NotFoundException, other_handler)To override the NotFoundException handler without affecting other 404s, use
the class-based registration only (no status-code registration).
15.3 Re-raising Unhandled Exceptions
Section titled “15.3 Re-raising Unhandled Exceptions”When no handler is found, wrap_http_exceptions logs the traceback and
re-raises. This means the exception propagates to the ASGI layer
(ServerErrorMiddleware), which produces a generic 500 response. In debug
mode, the 500 page includes the full traceback.
15.4 Pydantic ValidationError vs Tortoise ValidationError
Section titled “15.4 Pydantic ValidationError vs Tortoise ValidationError”These are different classes from different packages:
pydantic.ValidationError: handled bypydantic_validation_error_handler(422)tortoise.exceptions.ValidationError: handled byhandle_validation_error(422)
Both produce 422 responses but with different JSON shapes. The MRO lookup
correctly distinguishes them because they share no common base class (other
than Exception).
15.5 WebSocket Exceptions Don’t Go Through ExceptionMiddleware
Section titled “15.5 WebSocket Exceptions Don’t Go Through ExceptionMiddleware”WebSocketException is caught by WebSocketErrorMiddleware, not by
ExceptionMiddleware. The two middlewares operate at different levels of the
ASGI stack:
WebSocketErrorMiddlewareworks at the raw ASGI level (scope,receive,send).ExceptionMiddlewareworks at the sillo middleware level (request,response,call_next).
15.6 ResponseValidationError Is Intentionally Vague
Section titled “15.6 ResponseValidationError Is Intentionally Vague”The response_validation_error_handler deliberately returns a generic error
message. The actual validation errors are logged server-side but NOT included
in the response. This prevents leaking internal data that the response model
was supposed to filter out.
15.7 The handler is None Sentinel
Section titled “15.7 The handler is None Sentinel”In wrap_http_exceptions, the variable handler is initialized to None and
checked with if handler is None. This is NOT the same as if not handler.
The latter would also be True for falsy handler objects. The explicit is None
check is intentional.
16. Testing Exception Handlers
Section titled “16. Testing Exception Handlers”16.1 Unit Testing a Handler Directly
Section titled “16.1 Unit Testing a Handler Directly”import pytestfrom unittest.mock import AsyncMock, MagicMockfrom sillo.exceptions import NotFoundExceptionfrom sillo.handlers.not_found import handle_404_error
@pytest.mark.asyncioasync def test_handle_404_json(): request = MagicMock() request.headers = {"accept": "application/json"} request.accepts_json = True request.scope = {}
response = MagicMock() response.json = MagicMock(return_value="mock_response")
exc = NotFoundException("Resource not found") result = await handle_404_error(request, response, exc)
response.json.assert_called_once() call_args = response.json.call_args assert call_args[1]["status_code"] == 40416.2 Integration Testing via TestClient
Section titled “16.2 Integration Testing via TestClient”from sillo.testing import TestClient
def test_custom_exception_handler(): app = SilloApp()
@app.get("/fail") async def fail(request, response): raise HTTPException(400, detail="Bad request")
client = TestClient(app) resp = client.get("/fail") assert resp.status_code == 400 assert resp.json() == "Bad request"16.3 Testing MRO Resolution
Section titled “16.3 Testing MRO Resolution”def test_mro_resolution(): """Verify that the most specific handler is found.""" from sillo.exception_handler import _lookup_exception_handler
handlers = { HTTPException: handler_a, AuthenticationFailed: handler_b, }
# AuthenticationFailed is more specific exc = AuthenticationFailed() assert _lookup_exception_handler(handlers, exc) is handler_b
# Bare HTTPException matches the base handler exc = HTTPException(400) assert _lookup_exception_handler(handlers, exc) is handler_a
# An unrelated exception matches Exception if registered handlers[Exception] = handler_c exc = ValueError() assert _lookup_exception_handler(handlers, exc) is handler_c17. Source File Index
Section titled “17. Source File Index”| File | Lines | Purpose |
|---|---|---|
core/sillo/exceptions.py | 242 | HTTPException, NotFoundException, WebSocketException |
core/sillo/exception_handler.py | 407 | ExceptionMiddleware, wrap_http_exceptions, _lookup_exception_handler, all default handlers |
core/sillo/auth/exceptions.py | 197 | AuthException, AuthenticationFailed, PermissionDenied, AuthErrorHandler |
core/sillo/handlers/not_found.py | 162 | handle_404_error, generate_html_page, _debug_enabled, _prefers_html |
core/sillo/validation/errors.py | 126 | RequestValidationError, ResponseValidationError, prefix_errors |
core/sillo/validation/__init__.py | 89 | Re-exports for validation package |
core/sillo/record/exceptions.py | 110 | handle_does_not_exist, handle_integrity_error, handle_validation_error, handle_operational_error, register_db_exception_handlers |
core/sillo/websockets/errors.py | 40 | websocket_exception_handler, WebSocketErrorMiddleware |
core/sillo/types.py | 41 | ExceptionHandlerType type alias |
Appendix A: Complete Handler Registration Sequence
Section titled “Appendix A: Complete Handler Registration Sequence”sequenceDiagram
participant Dev as Developer
participant App as SilloApp
participant EM as ExceptionMiddleware
participant DB as record/exceptions
Dev->>App: SilloApp()
App->>EM: ExceptionMiddleware.__init__()
Note over EM: Registers 6 default handlers:<br/>HTTPException, AuthenticationFailed,<br/>NotFoundException, ValidationError,<br/>RequestValidationError,<br/>ResponseValidationError
Dev->>App: register_db_exception_handlers(app)
App->>DB: register_db_exception_handlers(app)
DB->>EM: add_exception_handler(DoesNotExist, ...)
DB->>EM: add_exception_handler(IntegrityError, ...)
DB->>EM: add_exception_handler(ValidationError, ...)
DB->>EM: add_exception_handler(OperationalError, ...)
Note over EM: _exception_handlers now has<br/>10 entries total
Dev->>App: app.add_exception_handler(429, rate_limit)
App->>EM: add_exception_handler(429, rate_limit)
Note over EM: _status_handlers now has<br/>1 entry
Appendix B: Exception Response Format Reference
Section titled “Appendix B: Exception Response Format Reference”HTTPException (generic)
Section titled “HTTPException (generic)”"Bad request"AuthenticationFailed
Section titled “AuthenticationFailed”"Authentication failed"NotFoundException (JSON mode, production)
Section titled “NotFoundException (JSON mode, production)”{ "status": 404, "error": "Not Found", "message": "The page you are looking for does not exist."}NotFoundException (JSON mode, debug)
Section titled “NotFoundException (JSON mode, debug)”{ "status": 404, "error": "Not Found", "message": "User with id 42 not found", "traceback": "Traceback (most recent call last):\n ..."}NotFoundException (HTML mode)
Section titled “NotFoundException (HTML mode)”<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>404 - Not Found</title> <style> body { font-family: Arial, sans-serif; text-align: center; margin: 50px; color: #333; } h1 { font-size: 48px; color: #d9534f; } p { font-size: 18px; margin-top: 10px; } </style></head><body> <h1>404 - Not Found</h1> <p>The page you are looking for does not exist.</p></body></html>NotFoundException (plain text mode)
Section titled “NotFoundException (plain text mode)”404 - Not FoundThe page you are looking for does not exist.Pydantic ValidationError
Section titled “Pydantic ValidationError”{ "error": "Validation Error", "errors": { "name": "field required", "address": { "city": "field required" } }}RequestValidationError
Section titled “RequestValidationError”{ "detail": [ { "loc": ["query", "page"], "msg": "value is not a valid integer", "type": "type_error.integer" } ]}ResponseValidationError
Section titled “ResponseValidationError”{ "error": "Internal Server Error", "detail": "Response validation failed"}DoesNotExist (DB)
Section titled “DoesNotExist (DB)”{ "error": "Not Found", "detail": "User matching query does not exist."}IntegrityError (DB)
Section titled “IntegrityError (DB)”{ "error": "Conflict", "detail": "UNIQUE constraint failed: user.email"}OperationalError (DB)
Section titled “OperationalError (DB)”{ "error": "Service Unavailable", "detail": "Database unavailable"}