Te veel verzoeken: wat de 429 vertelt
Hier bouw je op verder: Stoppen met break
Weet je dit niet meer zeker? Lees het terug voordat je verdergaat.
- Python Stoppen met break
Een bezoeker die over je limiet uit les 5 gaat, krijgt 429. In dat
antwoord staat nog iets meer, en je server kan ook al eerder laten weten hoeveel
verzoeken er nog over zijn. Een eerlijk script, zoals een app die je
gastenboek uitleest, kan daar rekening mee houden.
Het antwoord bij een 429
Zet deze regel onderaan test_verzoeken.py uit les 3:
print(antwoord.json())
Draai het script. De laatste regel is het antwoord op verzoek 20:
{'error': 'Rate limit exceeded: 5 per 1 minute'}
slowapi zegt welke grens het was: vijf per minuut. Wanneer het weer mag, staat
er niet bij.
Meer vertellen met headers_enabled
slowapi kan bij elk antwoord een paar headers meesturen: regels boven het
antwoord, die je in het tabblad Netwerk van je browser ook ziet. Zet ze aan met
headers_enabled=True. De headers zet slowapi op het antwoord, en daarvoor
krijgt je endpoint response: Response mee. Regel 1 laat zien waar Response
bij komt:
from fastapi import FastAPI, Request, Response
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address, headers_enabled=True)
app = FastAPI()
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
@app.get("/")
@limiter.limit("5/minute")
async def root(request: Request, response: Response):
return {"bericht": "Hallo wereld!"}
- Regel 1:
Responsekomt uitfastapi, net alsRequest, en komt daarom in deze regel erbij. - Regel 6:
headers_enabled=True: stuur bij elk antwoord mee hoe het met de limiet staat. - Regel 14:
Het endpoint krijgt een parameter
response: Response. Op dat antwoord zetslowapide headers.
Kijk wat er meekomt
Maak een script headers.py dat zes verzoeken stuurt en bij elk twee headers
laat zien:
import httpx
adres = "http://127.0.0.1:8000/"
for nummer in range(6):
antwoord = httpx.get(adres)
over = antwoord.headers.get("x-ratelimit-remaining")
wacht = antwoord.headers.get("retry-after")
print(f"Verzoek {nummer + 1}: {antwoord.status_code}, nog {over} over, wacht {wacht} seconden")
- Regel 7:
x-ratelimit-remaining: hoeveel verzoeken er deze minuut nog over zijn. - Regel 8:
retry-after: hoeveel seconden een bezoeker moet wachten tot de telling weer begint.
Start je server opnieuw, zodat de telling bij nul begint, en draai het script.
Bij een 200 hoef je niet te wachten: retry-after zegt dan alleen wanneer de
telling opnieuw begint. Pas bij een 429 moet je die tijd afwachten. Een
eerlijk script kijkt dus naar x-ratelimit-remaining en stopt bij nul, of wacht
na een 429 zo lang als retry-after zegt.
Draai je het script binnen dezelfde minuut nog een keer, dan krijg je al bij
verzoek 1 een 429, met nog 0 over en een wachttijd van hoogstens 60 seconden: de
telling van de vorige keer loopt nog.
Er gaat iets mis
None bij elk verzoek
Het script print bij elk verzoek nog None over, wacht None seconden, ook bij
de 429 van verzoek 6.
Oorzaak: je server draait nog de code uit les 5, zonder
headers_enabled=True. Dan stuurt hij de headers niet mee, en headers.get
geeft None als een header ontbreekt.
Oplossing: zet headers_enabled=True in de Limiter, en sla main.py op:
# FOUT
limiter = Limiter(key_func=get_remote_address)
# GOED
limiter = Limiter(key_func=get_remote_address, headers_enabled=True)
Zet er ook response: Response in je endpoint bij, anders krijg je de
volgende fout.
500 bij de eerste vijf verzoeken
Na headers_enabled=True geeft het script:
Verzoek 1: 500, nog None over, wacht None seconden
Verzoek 2: 500, nog None over, wacht None seconden
Verzoek 3: 500, nog None over, wacht None seconden
Verzoek 4: 500, nog None over, wacht None seconden
Verzoek 5: 500, nog None over, wacht None seconden
Verzoek 6: 429, nog 0 over, wacht 60 seconden
In de terminal van je server eindigt de foutmelding met:
Exception: parameter `response` must be an instance of starlette.responses.Response
Oorzaak: je endpoint heeft geen parameter response: Response. slowapi
wil de headers op het antwoord zetten, maar heeft er dan geen. De limiet telt
wel mee: ook een verzoek dat op 500 uitloopt, telt als een van je vijf. Het
zesde weigert slowapi dus al met 429, voordat je functie draait.
Oplossing: zet response: Response bij de parameters van elk endpoint met
een limiet dat een dictionary teruggeeft, en Response in de import:
# FOUT
async def root(request: Request):
# GOED
async def root(request: Request, response: Response):
Opdrachten
Opdracht 1: Predict - Na drie verzoeken
De limiet is 5/minute, met headers_enabled=True. Je stuurt drie verzoeken.
Vraag: wat staat er bij het derde antwoord in x-ratelimit-remaining?
Antwoord
2. Na drie van de vijf verzoeken zijn er nog twee over.
Opdracht 2: Make - Een script dat op tijd stopt
Pas headers.py aan zodat het uit zichzelf stopt zodra er nog 0 verzoeken over
zijn, in plaats van door te gaan tot een 429.
Tip
Met break stop je een for-loop halverwege.
Antwoord
import httpx
adres = "http://127.0.0.1:8000/"
for nummer in range(6):
antwoord = httpx.get(adres)
over = antwoord.headers.get("x-ratelimit-remaining")
print(f"Verzoek {nummer + 1}: {antwoord.status_code}, nog {over} over")
if over == "0":
print("Limiet bereikt, ik stop")
break
De header is tekst, dus je vergelijkt met "0" en niet met 0. Het script
stopt na het vijfde verzoek, en krijgt geen 429.
Opdracht 3: Make - In je eigen project
Zet in je eigen project een limiet op het endpoint dat een bericht opslaat, bijvoorbeeld tien per minuut. Test het met een script dat twaalf keer iets verstuurt.
Tip
Het endpoint heeft al parameters. Zet request: Request er vooraan bij, en
@limiter.limit onder @app.post. Installeer slowapi eerst in de
virtual environment van je eigen project.
Antwoord
Loop deze lijst af:
slowapiis geïnstalleerd in de virtual environment van je project.- Bovenaan
main.pystaan de imports en delimiter, en onderapp = FastAPI()de twee regels die hem aan je app hangen. - Het endpoint dat opslaat, heeft
@limiter.limitonder@app.post. request: Requeststaat vooraan bij de parameters; de rest blijft.- Een script dat twaalf keer verstuurt, krijgt na tien keer een
429.
Zet de imports en de limiter bij de andere imports, en Request bij de
import uit fastapi:
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address, headers_enabled=True)
Zet deze twee regels direct onder je bestaande app = FastAPI():
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
In het gastenboek met sessies verandert alleen de kop van je
POST /gastenboek. De parameters die er al stonden, ook sessie_id, blijven:
@app.post("/gastenboek")
@limiter.limit("10/minute")
async def gastenboek_opslaan(
request: Request,
bericht: str = Form(..., max_length=80),
sessie_id: str = Cookie(default=""),
):
...
Op de plek van ... blijft je functie zoals hij was. Die geeft een
RedirectResponse terug, of met het htmx-formulier een HTMLResponse. Allebei
zijn het antwoorden en geen dictionary, dus response: Response is hier niet
nodig: slowapi zet de headers op dat antwoord zelf.
Het script twaalf.py, naast je main.py. Het logt eerst in, want zonder
sessie krijgt elk bericht een 401; gebruik de naam en het wachtwoord van een
account dat je in je gastenboek hebt gemaakt:
import httpx
sara = httpx.Client(base_url="http://127.0.0.1:8000")
sara.post("/inloggen", data={"naam": "sara", "wachtwoord": "welkom123"})
for nummer in range(12):
antwoord = sara.post("/gastenboek", data={"bericht": f"Bericht {nummer + 1}"})
print(f"Verzoek {nummer + 1}: status {antwoord.status_code}")
Uitvoer, als je POST /gastenboek doorstuurt met een RedirectResponse:
Verzoek 1: status 303
Verzoek 2: status 303
Verzoek 3: status 303
Verzoek 4: status 303
Verzoek 5: status 303
Verzoek 6: status 303
Verzoek 7: status 303
Verzoek 8: status 303
Verzoek 9: status 303
Verzoek 10: status 303
Verzoek 11: status 429
Verzoek 12: status 429
303 is de omleiding naar de lijst: het bericht is opgeslagen. Gebruik je het
htmx-formulier, dan geeft je endpoint een HTMLResponse met "Bedankt", en staat
er bij verzoek 1 tot en met 10 status 200 in plaats van 303. Verzoek 11 en
12 krijgen ook dan een 429.
Een bezoeker die één bericht schrijft, merkt niets van de limiet. In je gastenboek staan nu wel tien testberichten.
Zo ziet je main.py er nu uit
from fastapi import FastAPI, Request, Response
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address, headers_enabled=True)
app = FastAPI()
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
@app.get("/")
@limiter.limit("5/minute")
async def root(request: Request, response: Response):
return {"bericht": "Hallo wereld!"}
De limiet vertelt nu bij elk antwoord hoeveel er nog over is.
Door naar les 7: in de praktijk.