Error Handling
Ushka turns every error into a proper HTTP response: expected errors (404, 400, ...) get an error page with the right status code, and unexpected exceptions become a 500 that shows details only in debug mode.
What happens by default
| Situation | Status |
|---|---|
| No route matches the path | 404 Not Found |
| The path exists, but not for this method | 405 Method Not Allowed, with an Allow header |
| Invalid JSON, UTF-8, query string or form data | 400 Bad Request |
A sanitiser rejects a value (RejectedValueError) |
400 Bad Request |
Body larger than the [limits] |
413 Payload Too Large |
| Missing static file | 404 Not Found |
Any other exception, including UnsafeValueError from Ushka Guard |
500 Internal Server Error |
Every error response is rendered with the error.html template.
Debug mode
With [app] debug = true (which ushka dev turns on for you):
- a 500 shows a debug page with the exception, the traceback, the code around
each frame and its local variables (names containing
password,secret,token,key,auth,credential,passorcookieare redacted); - a 404 lists the routes the app knows about;
- an
UnsafeValueErrorrequested withAccept: application/jsoncomes back as JSON with the errorcode, theriskand thefix, so a coding agent can correct the code (details).
With debug off (the default, and what you want in production) the client only sees a generic "Internal Server Error" page. The full traceback always goes to the log.
Danger
Never run production with debug on: the debug page shows source code and
local variables to anyone. ushka run warns about it and
ushka check --production fails.
Raising HTTP errors
Raise an exception from ushka.http.exceptions to stop the request and answer
with an error page:
# routes/posts/[post_id].py
from ushka.guard import safe
from ushka.http.exceptions import HttpNotFound
from ushka.templating import render
from models import Post
async def get(request, post_id):
post = await Post.get(safe.integer(post_id, min=1))
if post is None:
raise HttpNotFound(message="Sorry, we couldn't find that post.")
return await render(request, "post.html", {"post": post})
| Class | Default status and message |
|---|---|
HTTPError |
400, "Bad Request". The base class: pass any status_code. |
HttpNotFound |
404, "Not Found" |
HTTPBadRequest |
400, "Bad Request" |
HTTPPayloadTooLarge |
413, "Payload Too Large" |
HTTPMethodNotAllowed |
405, "Method Not Allowed"; takes allowed=[...] for the Allow header. |
HTTPStaticServerNotFound |
404, "Static File Not Found" |
They all accept template, status_code and message. Pass them as
keywords: the first positional argument is the template name, so
HttpNotFound("Post not found") would look for a template called
"Post not found".
For a status without its own class, use HTTPError:
from ushka.http.exceptions import HTTPError
async def delete(request):
if not request.session.get("is_admin"):
raise HTTPError(status_code=403, message="Admins only.")
...
To answer with an error without an HTML page (in a JSON API, for example), return a tuple instead of raising:
return {"error": "not found"}, 404
Custom error pages
Ushka looks up templates in your templates/ folder before its own, so to
replace the built-in error page create templates/error.html. It receives:
| Variable | Value |
|---|---|
status_code |
404, 500, ... |
message |
The exception's message. |
timestamp |
A datetime of when the error happened. |
available_urls |
In debug mode, for HTTP errors: (path, methods) pairs. Empty otherwise. |
request |
The current request. |
<!-- templates/error.html -->
<!DOCTYPE html>
<html>
<head><title>{{ status_code }} · {{ message }}</title></head>
<body>
{% if status_code == 404 %}
<h1>Oops! Page not found</h1>
<p>We couldn't find the page you were looking for.</p>
{% else %}
<h1>{{ status_code }}</h1>
<p>{{ message }}</p>
{% endif %}
<a href="/">Go back home</a>
</body>
</html>
The same template serves every status, including the production 500 page;
branch on status_code for different designs. The debug page
(debug_error.html) is only used in debug mode.
There is no automatic 404.html or 500.html. To use a different template
for one error, pass it when you raise:
raise HttpNotFound(template="post_missing.html", message="That post is gone.")