Error handling¶
BlackBull supplies a default handler for HTTP error statuses
and exceptions, so unhandled errors always produce
a response (no naked connection drops). You can override any of
them with @app.on_error(...).
Default behaviour¶
| Condition | Default response |
|---|---|
| Path not registered | 404 |
| Method not allowed | 405 + Allow header listing valid methods |
| Unhandled exception in handler | 500 |
The default handler adapts its body to the BLACKBULL_ENV
environment variable, the request's peer, and its Accept header.
development (default)¶
# BLACKBULL_ENV=development (or unset — development is the default)
The detail below is shown only to a loopback peer (127.0.0.0/8,
::1) whose request carries no Forwarded or X-Forwarded-For
header; the in-process test clients count as local. Any other
client, including one reached over a Unix socket, gets the
production body. A same-host relay that adds neither
header (a TCP-mode TLS terminator, an SSH tunnel, a sidecar) makes
every client look local, so set BLACKBULL_ENV=production on any
deployment.
For 500 errors with an exception:
Accept: text/html(browser) → styled HTML page with the full Python traceback inline.- Everything else (curl, fetch, etc.) → text/plain with the status line, exception class, exception message, and full traceback.
Goal: when you hit an unexpected 500 in dev, the failure point is visible in the response body — you don't have to dig through the access log.
For 404 / 405 / other client-error statuses without an exception,
the body is just the status code + phrase (with the Allow header
on 405).
4xx HTTPExceptions carry their detail, not a traceback (since
v0.56.0): when the framework itself diagnoses a client fault — a
missing required query parameter, a malformed JSON body — the dev
page shows the status and the human-readable detail line
(missing required query parameter 'q' for handler 'search') and
omits the Python traceback. The frames would only show framework
internals; the detail line is the actionable part. 5xx
HTTPExceptions and unexpected exceptions keep the full traceback.
production¶
BLACKBULL_ENV=production python myapp.py
Terse output — status code + phrase only. Exception class and message are not leaked to the network. Browsers get a minimal HTML page; everything else gets text/plain.
This is the right posture for public endpoints: production users of your service shouldn't see Python internals when something fails. Log the exception server-side via your normal logging / observability pipeline.
The same rule applies to the Content-Type and Content-Length
headers — both are set explicitly on every error response in both
environments.
Custom error handlers¶
@app.on_error(...) registers a handler for a specific status
code or exception type:
from http import HTTPStatus
from blackbull import JSONResponse
@app.on_error(HTTPStatus.NOT_FOUND)
async def handle_404(conn, receive, send):
await send(JSONResponse({'error': 'not found'},
status=HTTPStatus.NOT_FOUND))
@app.on_error(HTTPStatus.METHOD_NOT_ALLOWED)
async def handle_405(conn, receive, send):
allowed = ', '.join(conn.state.get('allowed_methods', ()))
await send(JSONResponse({'error': f'allowed: {allowed}'},
status=HTTPStatus.METHOD_NOT_ALLOWED))
@app.on_error(ValueError)
async def handle_value_error(conn, receive, send):
exc = conn.state.get('error_exception')
await send(JSONResponse({'error': str(exc)},
status=HTTPStatus.BAD_REQUEST))
Exceptions, including validation errors and route guards, select:
- The most specific explicitly registered exception handler along the MRO.
- A handler for the resolved status.
- The framework default.
An explicit @app.on_error(Exception) takes priority over status handlers.
Route lookup 404/405 uses status handlers directly.
What's in conn.state¶
The framework populates conn.state with information about
the error before calling your handler:
| Key | Type | Present when |
|---|---|---|
'error_status' |
HTTPStatus |
Always |
'error_exception' |
Exception |
Triggered by an uncaught exception |
'allowed_methods' |
tuple of str |
405 Method Not Allowed |
Reading these is how a custom handler builds a response that matches the framework's view of what failed.
Custom HTML error pages¶
To customize the look of the DEV-mode traceback page or the PROD-mode minimal page, register a handler for the status code you want to override:
from blackbull import Response
@app.on_error(HTTPStatus.INTERNAL_SERVER_ERROR)
async def custom_500(conn, receive, send):
await send(Response(b'<h1>Something went wrong</h1>',
status=HTTPStatus.INTERNAL_SERVER_ERROR,
content_type='text/html'))
This handles exceptions resolving to 500 unless an exception handler takes priority.