Ga naar hoofdinhoud

Doorsturen na opslaan (redirect)

Je gastenboek werkt, maar na het versturen van een bericht sta je op een doodlopende pagina. Druk je daar op F5, dan vraagt je browser of je het formulier nog een keer wilt versturen. Zeg je ja, dan staat hetzelfde bericht twee keer in je database. Daarom stuur je de bezoeker na het opslaan door naar de berichten.

RedirectResponse​

Met RedirectResponse stuur je de browser door naar een andere URL. Hij komt uit fastapi.responses, net als FileResponse en HTMLResponse. Zet hem daarom in die regel bovenaan je main.py:

main.pydraait op de server
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse

Vervang dan je POST-endpoint /gastenboek uit Elk bericht bewaren: een eigen sleutel. Alleen de laatste regel verandert:

main.pydraait op de server
@app.post("/gastenboek")
async def gastenboek_opslaan(naam: str = Form(...), bericht: str = Form(...)):
with SqliteDict("gastenboek.db") as db:
sleutel = f"bericht_{time.time_ns()}"
db[sleutel] = {"naam": naam, "bericht": bericht}
db.commit()
return RedirectResponse(url="/berichten", status_code=303)
  1. Regel 3-6:

    Het opslaan is hetzelfde als in Elk bericht bewaren: een eigen sleutel: een eigen sleutel per bericht, met import time bovenaan je bestand.

  2. Regel 7:

    Het endpoint geeft nu geen pagina terug, maar een opdracht aan de browser: ga naar /berichten. De browser doet dat meteen, en de bezoeker ziet zijn eigen bericht achteraan staan. Nu nog als JSON; in de volgende les wordt /berichten een echte pagina.

Waarom 303​

Zonder status_code=303 werkt het niet. RedirectResponse stuurt standaard een 307, en 307 betekent: doe hetzelfde verzoek nog een keer op de nieuwe URL. Jouw verzoek was een POST, dus de browser stuurt een POST naar /berichten. Dat endpoint is een @app.get, dus je krijgt 405 Method Not Allowed.

Een 303 zegt wél wat je bedoelt: ga naar die URL met een GET.

Eerst een POST en dan doorsturen naar een GET: dat heet het post/redirect/get-patroon. Bijna elke website die je gebruikt doet dit na een formulier.

Testen​

  1. Vul het gastenboek in en klik Verstuur
  2. Je belandt op /berichten en ziet je eigen bericht staan, nog als JSON
  3. Druk op F5. De pagina herlaadt, zonder vraag over opnieuw versturen
Gaat er iets mis?

Krijg je 405 Method Not Allowed meteen na het versturen? Dan mist status_code=303. Meer oplossingen staan op Er gaat iets mis.

Zo ziet je main.py er nu uit

Dit is je hele main.py na deze les, zonder wat je in de opdrachten maakte.

main.pydraait op de server
import random
import time

from fastapi import FastAPI, Form, Request
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from sqlitedict import SqliteDict

app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")

@app.get("/")
async def home():
return FileResponse("static/pages/home.html")

@app.get("/info")
async def info():
return {"naam": "Jouw Naam", "leeftijd": 16}

@app.get("/pagina", response_class=HTMLResponse)
async def pagina():
return """
<!DOCTYPE html>
<html>
<head>
<title>Mijn Pagina</title>
</head>
<body>
<h1>Hallo!</h1>
<p>Dit is mijn eerste HTML pagina.</p>
</body>
</html>
"""

@app.get("/styled", response_class=HTMLResponse)
async def styled():
return """
<!DOCTYPE html>
<html>
<head>
<title>Met CSS</title>
<style>
h1 { color: blue; }
p { color: gray; }
</style>
</head>
<body>
<h1>Gestylede pagina</h1>
<p>Deze tekst is grijs.</p>
</body>
</html>
"""

@app.get("/about")
async def about():
return FileResponse("static/pages/about.html")

DIEREN = {
"kat": "Een kat spint als hij tevreden is.",
"hond": "Een hond kwispelt als hij blij is.",
"vis": "Een vis ademt met kieuwen.",
}

@app.get("/dier/{naam}", response_class=HTMLResponse)
async def dier(naam: str):
tekst = DIEREN.get(naam, "Dit dier staat nog niet op de site.")
return f"""
<!DOCTYPE html>
<html>
<body>
<h1>Over dieren</h1>
<p>{tekst}</p>
<a href="/">Terug naar Home</a>
</body>
</html>
"""

@app.get("/zoek")
async def zoek(term: str):
return {"je_zocht": term}

@app.get("/foto")
async def foto():
return FileResponse("static/pages/foto.html")

@app.get("/dobbelsteen")
async def dobbelsteen(request: Request):
worp = random.randint(1, 6)
return templates.TemplateResponse(
request,
"dobbelsteen.html",
{"worp": worp}
)

@app.get("/naam")
async def toon_formulier():
return FileResponse("static/pages/naam_form.html")

@app.post("/verstuur")
async def ontvang_naam(naam: str = Form(...)):
print(f"Naam ontvangen: {naam}")
return {"bericht": f"Hallo {naam}!"}

@app.get("/groet")
async def groet_form():
return FileResponse("static/pages/groet_form.html")

@app.post("/groet")
async def groet_verwerk(request: Request, naam: str = Form(...)):
return templates.TemplateResponse(
request,
"groet_resultaat.html",
{"naam": naam}
)

@app.get("/opslaan")
async def naam_formulier():
return FileResponse("static/pages/naam_opslaan.html")

@app.post("/opslaan")
async def naam_opslaan(naam: str = Form(...)):
with SqliteDict("namen.db") as db:
db["laatste_naam"] = naam
db.commit()

return {"bericht": f"'{naam}' opgeslagen!"}

@app.get("/toon_naam")
async def toon_naam():
with SqliteDict("namen.db") as db:
naam = db.get("laatste_naam", "Nog niets opgeslagen")
return {"naam": naam}

@app.get("/gastenboek")
async def gastenboek_form():
return FileResponse("static/pages/gastenboek_form.html")

@app.post("/gastenboek")
async def gastenboek_opslaan(naam: str = Form(...), bericht: str = Form(...)):
with SqliteDict("gastenboek.db") as db:
sleutel = f"bericht_{time.time_ns()}"
db[sleutel] = {"naam": naam, "bericht": bericht}
db.commit()
return RedirectResponse(url="/berichten", status_code=303)

@app.get("/berichten")
async def berichten():
with SqliteDict("gastenboek.db") as db:
alle_berichten = list(db.values())
return {"berichten": alle_berichten}
Controlevraag
Je verstuurt een bericht met het endpoint uit deze les en kijkt in het tabblad Netwerk. Welke verzoeken zie je?
Kies wat jij denkt. Bij het goede antwoord zie je de uitleg.
Controleer je server

Laat je server draaien en typ in een tweede terminal python controleer.py berichten-opslaan. Het script kijkt of alles uit dit hoofdstuk werkt; je downloadt het op Project checken.

Eerst op papier

Speel na wat er bij één klik heen en weer gaat, met briefjes en zonder computer: Het verzoek-rollenspel. Je docent kan de hand-out printen.

Opdrachten​

Opdracht 1: Predict - Waar kom je uit?​

main.pydraait op de server
@app.post("/inschrijven")
async def inschrijven(naam: str = Form(...)):
return RedirectResponse(url="/bedankt", status_code=303)

@app.get("/bedankt")
async def bedankt():
return {"bericht": "Bedankt"}
Voorspel
Welke URL staat er in de adresbalk nadat je het formulier hebt verstuurd?
Kies wat jij denkt. Bij het goede antwoord zie je de uitleg.

Opdracht 2: Run​

Pas je /gastenboek POST-endpoint aan zodat het doorstuurt naar /berichten. Verstuur een bericht en kijk of je op de lijst uitkomt.

Opdracht 3: Investigate - Zonder 303​

Haal status_code=303 weg en verstuur nog een keer.

Antwoord

Je krijgt 405 Method Not Allowed. Zonder dat getal stuurt FastAPI een 307, en dan herhaalt de browser je POST op /berichten. Daar staat alleen een @app.get, dus het antwoord is: dit endpoint doet geen POST. Je bericht is wel opgeslagen; alleen het doorsturen ging mis. Ga je terug en verstuur je hetzelfde formulier nog een keer, dan staat het er twee keer in.

Opdracht 4: Make - Annuleren​

Maak een endpoint /annuleren dat niets opslaat en de bezoeker direct doorstuurt naar /berichten. Zet er een link naartoe op je gastenboekformulier.

Tip

Een bezoeker komt hier via een gewone link, dus het is een @app.get. Een redirect vanuit een GET heeft geen 303 nodig.

Antwoord
main.pydraait op de server
@app.get("/annuleren")
async def annuleren():
return RedirectResponse(url="/berichten")

Zet onder het formulier:

static/pages/gastenboek_form.htmlgaat zoals hij is naar de browser
<a href="/annuleren">Annuleren</a>

Hier mag de standaard 307 blijven staan: het oorspronkelijke verzoek was al een GET, dus het herhalen daarvan levert precies op wat je wilt.

Door naar Alles tonen: een for-lus in je template.