DocsReference
Exceptions and errors
A request can fail in four places, and Nexy answers each one differently. This page is the index: find the status you see, then the line that produced it, then the file that decides the body.
The short version
| What happened | Status | Browser sees | API client sees |
|---|---|---|---|
| no route matches the URL | 404 |
notfound.nexy, else the built-in page |
{"detail": "Not Found"} |
| a path segment does not fit its annotation | 404 |
the same page | {"detail": "Not Found"} |
| a query, header, cookie or body field is missing or invalid | 422 |
FastAPI's JSON body | FastAPI's JSON body |
notFound() |
404 |
the page | {"detail": ...} |
unauthorized() |
401 |
the built-in page | {"detail": ...} |
forbidden() |
403 |
the built-in page | {"detail": ...} |
raise HTTPException(409, ...) |
409 |
the built-in page | {"detail": ...} |
| an exception nothing caught | 500 |
errors.nexy, else the built-in page |
{"detail", "reference"} |
redirect(url) |
307 |
the destination, no page | the destination, no body |
permanentRedirect(url) |
308 |
the destination, no page | the destination |
An API client is a request whose Accept header asks for JSON and not for a page:
curl -H "Accept: application/json" http://localhost:3000/api/items/500
# {"detail":"Out of range"}
Where a failure comes from
Browser ──► Middleware ──► Router ──► Page or handler
│ │ │
│ │ ├─ return ───────────► 200
│ │ ├─ HTTPException ────► its status
│ │ └─ other exception ───► 500
│ └─ no match ────────────────────────► 404
└─ raises ──────────────────────────────────────► 500
| Stage | What can fail | The answer |
|---|---|---|
| Middleware | your middleware.py, CORS, sessions |
the exception's status, else 500 |
| Router | the URL matches no file | 404 |
| Validation | a path segment cannot be converted | 404 |
| Validation | a query, header, cookie or body field | 422 |
| Page, handler, action, guard | HTTPException |
its status |
| Page, handler, action, guard | anything else | 500 |
Leave from anywhere
redirect, permanentRedirect, notFound, forbidden and unauthorized raise an HTTP exception,
so the same call works in a .nexy page, an API handler, a modular controller, a guard, a
dependencies.py and an action.
from nexy import notFound, redirect
from src.posts import find
async def GET(slug: str):
post = find(slug)
if post is None:
notFound("No such post")
if post.moved_to:
redirect(post.moved_to)
return post| Call | Answer | When |
|---|---|---|
notFound(detail=None) |
404 |
the resource does not exist |
unauthorized(detail=None, scheme=None) |
401 |
the visitor must sign in first |
forbidden(detail=None) |
403 |
the visitor is known but may not see this |
redirect(url, status=307) |
307 |
send them elsewhere, keeping the method |
redirect(url, status=303) |
303 |
after a form: the next request is a GET |
permanentRedirect(url) |
308 |
a moved page that search engines should remember |
Build the destination from values you trust. redirect(request.query_params["next"]) sends visitors
wherever the link says; it is an open redirect. A destination that holds a line break is refused.
Validation: 422, or 404
Handler parameters are validated by FastAPI against their annotations.
from nexy.http import Header
async def GET(q: str, limit: int = 10, x_token: str = Header(...)):
return {"q": q, "limit": limit}| Problem | Status | Why |
|---|---|---|
| missing or invalid query, header, cookie or body field | 422 |
the request reached the handler, the data is wrong |
invalid path segment (/api/users/abc for id: int) |
404 |
the folder has no route for a non-number, so the URL does not exist |
The 422 body is FastAPI's standard shape, so clients and generated SDKs keep working:
{"detail": [{"loc": ["query", "q"], "msg": "Field required", "type": "missing"}]}
For a form or an action, the client receives an ActionError with status and detail; a 422
also fills error.fields with one message per field. See Actions.
Custom error pages
<h1>Page not found</h1>
<p>The link may be old. <a href="/">Start from the home page</a>.</p><h1>Something went wrong</h1>
<p>Try again in a moment.</p>| File | Status | Scope |
|---|---|---|
src/routes/notfound.nexy |
404 |
every route of the project |
src/routes/errors.nexy |
500 |
every route of the project |
src/routes/forbidden.nexy |
403 |
every route of the project |
src/routes/unauthorized.nexy |
401 |
every route of the project |
src/routes/blog/notfound.nexy |
404 |
/blog and below |
src/routes/blog/errors.nexy |
500 |
/blog and below |
src/routes/loading.nexy |
- | reserved, no runtime effect yet |
One file somewhere in the routes installs the answer for its status: a copy at the root answers the whole project, a copy in a folder answers that folder, and the deepest file that matches the URL wins. A URL outside every copy, and a status with no file at all, get the built-in styled pages. See Error pages per folder.
Unhandled exceptions
An exception that nothing caught is logged once, with its reference, where it came from and the code
around it. The status is 500, and the body depends on the environment.
nexy dev |
nexy build then nexy start |
|
|---|---|---|
| Browser | the error, the lines of your code and the traceback | errors.nexy, else the built-in page, with a reference |
| API client | {"detail", "reference", "error"} |
{"detail", "reference"} |
| Terminal | the exception and the lines of your code | the exception, the whole traceback and the reference |
x ZeroDivisionError: division by zero GET /boom ref fe6a5217
at src/routes/boom.py:2 in GET
1 | def GET():
2 | return {"a": 1 / 0}
The reference is eight characters, new for each failure. A visitor can quote it, and you find the traceback by searching the log for it. Neither the exception nor the code reaches the visitor in production.
Actions and forms
An action is a POST endpoint with a generated path. Its failures follow the same rules:
| Situation | Status | Client |
|---|---|---|
| a field is missing or invalid | 422 |
ActionError with fields |
HTTPException(403) raised in the action |
403 |
ActionError with status and detail |
| an unhandled exception | 500 |
ActionError; the reference is in the server log |
the Origin header is another site |
403 |
refused before the action runs |
A form without JavaScript posts to the action path; a success answers 303 back to the page, a
failure shows the error page. See Actions.
A checklist when a page answers the wrong thing
- Look at the status, not the page. A custom
notfound.nexystill answers404. - Read the terminal. A page that fails to import is not registered, so its URL answers
404while the file exists. The import error is in the log. - Check the extension. With
useCSR = True,.nexypages belong to server mode; a page in the wrong mode is not a route. - Check the depth. A
notfound.nexyorerrors.nexyonly works when the root copy exists. - Ask for JSON with
Accept: application/jsonto see the shape your API client receives. - Quote the reference of a
500when you open an issue or search the log.
Next steps
- Error pages: the full page behaviour, with examples
- Validation and errors: API handlers
- Troubleshooting: the errors people actually hit
- Testing: assert a status and a body