Ga naar hoofdinhoud

Een eigen 404-pagina: een handler voor fouten

Typt een bezoeker een adres dat niet bestaat, dan ziet hij {"detail":"Not Found"}. Voor jezelf is dat duidelijk genoeg. Laat je je gastenboek aan anderen zien, dan wil je een pagina in je eigen stijl, met een link terug naar de berichten.

Twee soorten 404​

Voorspel
Je gastenboek heeft de 404 uit Een 404 sturen. Wat zie je op /bestaat-niet, en wat op /bericht/onzin?
Kies wat jij denkt. Bij het goede antwoord zie je de uitleg.

Allebei zijn het een 404, en allebei moeten ze straks je eigen pagina tonen. Een andere fout, zoals een fout wachtwoord (401) of een fout in je code (500), moet blijven wat hij is.

Een handler voor 404​

Een exception handler is een functie die FastAPI aanroept als er een fout ontstaat, in plaats van zelf JSON te sturen. Jij maakt er een voor 404: een onbekend adres en jouw eigen raise HTTPException(status_code=404) gaan er allebei doorheen, en een 401, een 405 of een fout in je code blijven wat ze zijn.

Die bouw je hieronder. Er zijn nog andere manieren; waarom deze de beste keuze is, lees je onder Achtergrond.

Achtergrond: twee andere manieren

Je kunt de handler ook aan FastAPI() meegeven, als app = FastAPI(exception_handlers={404: niet_gevonden}), met de functie boven die regel. Dat doet precies hetzelfde. Er zijn ook twee manieren die echt anders werken.

Een handler voor elke HTTP-fout. Een handler voor StarletteHTTPException krijgt elke fout die FastAPI of jij met een statuscode stuurt: 404, maar ook 401 en 405. Daarom kijkt hij zelf naar de code, en geeft hij de rest door aan de handler die FastAPI normaal gebruikt:

main.pydraait op de server
from fastapi.exception_handlers import http_exception_handler
from starlette.exceptions import HTTPException as StarletteHTTPException

@app.exception_handler(StarletteHTTPException)
async def http_fouten(request: Request, fout):
if fout.status_code == 404:
return templates.TemplateResponse(
request,
"404.html",
{"pad": request.url.path},
status_code=404,
)
return await http_exception_handler(request, fout)

De await wacht op het antwoord van die andere handler. Meer hoef je er hier niet over te weten.

Vergeet je de if en de laatste regel, dan toont je server bij een fout wachtwoord of een verkeerde methode ook "Niet gevonden", met status 404. Een bezoeker zoekt dan naar een pagina die er wel is.

Een pagina in je eigen endpoint. In bericht_detail kun je in plaats van raise HTTPException zelf de pagina teruggeven:

main.pydraait op de server
if bericht is None:
return templates.TemplateResponse(
request,
"404.html",
{"pad": request.url.path},
status_code=404,
)

Dat werkt alleen voor dit ene endpoint. Een adres zonder endpoint geeft nog steeds JSON, want daar draait geen functie van jou.

Handler voor 404Handler voor elke HTTP-foutIn je eigen endpoint
Onbekend adresjouw paginajouw paginaJSON
Jouw raise HTTPException(status_code=404)jouw paginajouw paginaalleen waar je het verandert
401, 405blijven JSONblijven JSON, als je ze doorgeeftblijven JSON
Fout in je code (500)blijft een 500blijft een 500blijft een 500
Wat je schrijftéén functieeen functie, een if en twee importsdezelfde regels in elk endpoint

De handler voor 404 doet wat je wilt, met de minste code, en raakt de andere fouten niet.

De handler bouwen​

Maak een nieuwe template:

templates/404.htmlde server vult hem in, de browser toont hem
<!DOCTYPE html>
<html>
<head>
<title>Niet gevonden</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<h1>Niet gevonden</h1>
<p>Er staat niets op {{ pad }}.</p>
<a href="/berichten">Naar alle berichten</a>
</body>
</html>

Zet de handler onder je andere endpoints:

main.pydraait op de server
@app.exception_handler(404)
async def niet_gevonden(request: Request, fout):
return templates.TemplateResponse(
request,
"404.html",
{"pad": request.url.path},
status_code=404,
)
  1. Regel 1:

    @app.exception_handler(404) lijkt op @app.get, maar hoort niet bij een pad. Hij hoort bij een statuscode: bij elke 404, waar die ook vandaan komt.

  2. Regel 2:

    FastAPI geeft het verzoek en de fout zelf mee. fout gebruik je hier nog niet; in opdracht 4 wel.

  3. Regel 6:

    request.url.path is het pad dat de bezoeker opvroeg, zoals /bestaat-niet.

  4. Regel 7:

    Zonder deze regel krijgt de pagina status 200: dan lijkt het voor de browser en voor elk script alsof de pagina bestaat.

templates en Request heb je sinds Templates met Jinja2 al.

Testen​

  1. Ga naar http://127.0.0.1:8000/bestaat-niet. Je ziet je eigen pagina, met "Er staat niets op /bestaat-niet." In het tabblad Netwerk staat status 404.
  2. Ga naar /bericht/onzin. Ook daar staat je pagina: je raise HTTPException(status_code=404, …) gaat door dezelfde handler.
  3. Log in met een fout wachtwoord. Daar staat nog steeds {"detail":"Naam of wachtwoord klopt niet"}, met status 401.

Een fout in je code blijft een 500​

Zit er een fout in je Python, dan krijgt de bezoeker Internal Server Error, met status 500, en staat de foutmelding in je terminal. Dat is niet mooi, maar wel eerlijk: een 500 zegt dat het aan je server ligt, en jij weet dat je in de terminal moet kijken.

Je wilt daar misschien ook een nette pagina voor, met een handler voor Exception, die elke fout in Python opvangt. Doe dat niet terwijl je nog bouwt. Je browser toont dan een pagina die er goed uitziet, en jij zoekt de fout op de verkeerde plek. Wat er dan gebeurt, zie je in opdracht 3.

Controlevraag
Je zet 404.html per ongeluk in static/pages/ in plaats van in templates/. De handler laat je zoals hij is. Wat zie je in Netwerk als je /bestaat-niet opent?
Kies wat jij denkt. Bij het goede antwoord zie je de uitleg.

Er gaat iets mis​

Netwerk zegt 200 bij je foutpagina​

Oorzaak: status_code=404 ontbreekt in de TemplateResponse. Dan stuurt je server je pagina met de standaardcode 200: alles goed. De bezoeker ziet het verschil niet, maar een script of een zoekmachine denkt dat de pagina bestaat.

Oplossing: geef de status mee als laatste argument:

main.pydraait op de server
return templates.TemplateResponse(
request,
"404.html",
{"pad": request.url.path},
status_code=404,
)

Elk onbekend adres geeft Internal Server Error​

In je terminal staat onderaan:

jinja2.exceptions.TemplateNotFound: '404.html' not found in search path: 'templates'

Oorzaak: je handler vindt zijn template niet. Hij draait bij elke 404, dus elke 404 wordt een 500.

Oplossing: zet 404.html in de map templates, naast je andere templates, en controleer de bestandsnaam.

Een onbekend adres geeft nog steeds JSON​

Oorzaak: je handler is voor HTTPException uit fastapi, in plaats van voor 404 of voor StarletteHTTPException. Die vangt alleen je eigen raise HTTPException. Een adres zonder endpoint geeft FastAPI zelf, met de HTTPException uit Starlette, en daar komt je handler niet langs.

Oplossing: gebruik @app.exception_handler(404).

Controleer je server

Laat je server draaien en typ in een tweede terminal python controleer.py afronden. Het script kijkt of een onbekend adres je eigen pagina met een 404 geeft, en of andere fouten JSON blijven; je downloadt het op Project checken.

Opdrachten​

Opdracht 1: Predict - Een verkeerde methode​

/verwijderen bestaat alleen als POST. Iemand typt het adres in zijn adresbalk, en de browser stuurt een GET.

Voorspel
Ziet hij je 404-pagina?
Kies wat jij denkt. Bij het goede antwoord zie je de uitleg.

Opdracht 2: Run​

Maak 404.html en de handler, en open drie adressen: een die niet bestaat, /bericht/onzin en /berichten. Welke statuscode geeft Netwerk bij elk?

Antwoord

404, 404 en 200. De eerste twee tonen je eigen pagina. /berichten bestaat, dus daar komt je handler niet aan te pas.

Opdracht 3: Investigate - Een handler voor alles​

Vervang tijdelijk @app.exception_handler(404) door @app.exception_handler(Exception). Zet daarna in je GET /berichten een tikfout: verander alle_berichten in de return in alle_berichtn. Open /berichten. Wat ziet je browser, wat zegt Netwerk, en wat staat er in je terminal? Zet daarna alles terug.

Antwoord

Je browser toont de kop "Niet gevonden" met "Er staat niets op /berichten." eronder, met status 404, terwijl die pagina wel bestaat. In je terminal staat de echte fout: NameError: name 'alle_berichtn' is not defined. Wie alleen naar de browser kijkt, zoekt een ontbrekende pagina en niet een tikfout.

Probeer ook /bestaat-niet: daar staat weer {"detail":"Not Found"}. Een 404 is in FastAPI geen gewone fout in Python, dus een handler voor Exception vangt hem niet. Deze handler doet het precies verkeerd om.

Opdracht 4: Make - De melding erbij​

Laat je pagina ook de melding van de 404 zien. Bij /bericht/onzin staat er dan "Dit bericht bestaat niet". Bij een onbekend adres geeft FastAPI de Engelse melding Not Found; zet daar "Deze pagina bestaat niet" voor in de plaats.

Tip

De melding staat in fout.detail. Geef hem mee aan de template, net als het pad, en vergelijk hem eerst met "Not Found".

Antwoord
main.pydraait op de server
@app.exception_handler(404)
async def niet_gevonden(request: Request, fout):
melding = fout.detail
if melding == "Not Found":
melding = "Deze pagina bestaat niet"
return templates.TemplateResponse(
request,
"404.html",
{"pad": request.url.path, "melding": melding},
status_code=404,
)

In templates/404.html, onder de kop:

templates/404.htmlde server vult hem in, de browser toont hem
<p>{{ melding }}</p>