DocsGo deeper
Error pages
A request that fails should still answer like the rest of your site: a page in your own words for a visitor, the same failure as JSON for an API client. Nexy answers both, and one file replaces each answer.
This page is the map: which file answers which status, and how to make your first one. The pages of a folder get their own page.
One failure, two answers
a request that fails
│
├── a browser (Accept: text/html) ──► an HTML page
└── an API client (Accept: application/json) ──► {"detail": "…"} with the same status
the HTML page
├── the deepest folder of the URL that holds a file ──► your file
└── no folder of the chain holds one ──► the built-in page
Your first page
Create src/routes/notfound.nexy and write what a visitor should read:
---
title: Page not found
---
<h1>Page not found</h1>
<p>The page moved or never existed. <a href="/">Back home</a>.</p>Save, and ask for a URL nothing serves:
curl -i -H "Accept: text/html" http://localhost:3000/nowhere
# HTTP/1.1 404 Not Found
The file at the routes root answers the whole project; a copy in a folder answers only that folder and keeps the rest as it is. Without any file, Nexy serves its built-in styled page.
Which file answers
| File | Answers |
|---|---|
notfound.nexy |
404 for its directory |
errors.nexy |
500 for its directory |
forbidden.nexy |
403 for its directory |
unauthorized.nexy |
401 for its directory |
loading.nexy |
reserved; no runtime effect yet |
| Situation | Answer |
|---|---|
| No route matches | 404 |
| A dynamic segment cannot be converted to its annotation | 404 |
| An unhandled exception, in a page or an API handler | 500 |
raise HTTPException(status_code=403) |
the status you raised, as HTML or JSON |
notFound(), forbidden(), unauthorized() |
404, 403, 401, from anywhere in a request |
redirect(), permanentRedirect() |
a real redirect with no error page |
Leave a page or answer from code
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 and a
dependencies.py. They have the same names on the client (@nexy/client).
---
from nexy import notFound, redirect
from src.posts import find
slug: prop[str]
post = find(slug)
if post is None:
notFound()
if post.moved_to:
redirect(post.moved_to)
---
<h1>{{ post.title }}</h1>| Call | Answer |
|---|---|
redirect(url) |
307; redirect(url, status=303) turns a POST into a GET after a form |
permanentRedirect(url) |
308; browsers and search engines remember it |
notFound(detail=None) |
404: the notfound page, or JSON for an API client |
forbidden(detail=None) |
403: the visitor is known but may not see this |
unauthorized(detail=None, scheme=None) |
401; scheme="Bearer" adds WWW-Authenticate |
A redirect answers with the Location header and no page, also to an API client. Build the
destination from values you trust: a redirect(request.query_params["next"]) sends visitors
wherever the link says. A destination with a line break is refused.
A guard protects a whole folder:
from nexy import unauthorized
from nexy.http import Request
def require_login(request: Request) -> None:
if not request.session.get("user"):
unauthorized(scheme="Bearer")
dependencies = [require_login]HTML or JSON
The same exception answers differently depending on who asks:
- A browser (
Accept: text/html) gets an HTML page. The detail is HTML-escaped, so a message built from user input cannot inject markup. - A client that asks for JSON and not for a page (
Accept: application/json) gets{"detail": "..."}with the same status code.
from nexy.http import HTTPException
async def GET(id: int):
if id > 100:
raise HTTPException(status_code=403, detail="Out of range")
return {"id": id}curl -H "Accept: application/json" http://localhost:3000/api/items/500
# {"detail":"Out of range"}
Unhandled exceptions
An exception that nothing handled is logged once, with the exception, where it came from and the code around it, and answered according to the mode.
nx dev |
nx start |
|
|---|---|---|
| Browser | a page with the error, the lines of your code that raised it and the full traceback | the built-in 500 page (or your errors.nexy) with a reference |
| JSON client | {"detail", "reference", "error"} |
{"detail", "reference"} |
| Server log | the exception and the lines of your code | the exception and the whole traceback, with the same 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. A custom errors.nexy replaces the page only; the log line stays.
Validation errors
A missing header, query parameter or body field answers 422 with FastAPI's JSON body, for every
client. See Validation and errors.
Next steps
- Error pages per folder: a 404, 403 or 401 of your own in one folder
- Dynamic routes
- API routes
- Testing