Te veel verzoeken: een limiet met slowapi
In les 3 beantwoordde je server elk verzoek. Je kunt niet voorkomen dat iemand veel verzoeken stuurt. Wat je wél kunt, is weigeren om ze allemaal te beantwoorden. Krijgt één bezoeker meer dan een paar verzoeken per minuut, dan is dat geen mens die een pagina leest, maar een script.
Een grens op het aantal verzoeken per bezoeker heet een rate limit, een verzoeklimiet. Hij stopt één computer die te veel vraagt, of die zijn verzoeken nu na elkaar of tegelijk stuurt. Tegen een DDoS van duizenden computers helpt hij minder, want elk apparaat blijft onder de grens; wat daartegen helpt, zie je in les 7.
slowapi installeren
De module slowapi houdt de telling voor je bij. Installeer hem in de terminal
van VS Code:
python -m pip install slowapi
De limiet in je server
from fastapi import FastAPI, Request
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)
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):
return {"bericht": "Hallo wereld!"}
- Regel 1-4:
Requestkomt erbij in de regel uitfastapi, want de limiet moet het verzoek zien. De andere drie regels komen vanslowapi. - Regel 6:
De
Limitertelt de verzoeken.get_remote_addresszegt: tel per computer, aan de hand van zijn adres. Voor jouw script is dat127.0.0.1. - Regel 9-10:
Je hangt de limiter aan je app, en zegt wat er moet gebeuren als iemand over de grens gaat: het antwoord
429 Too Many Requests. - Regel 13:
@limiter.limit("5/minute")direct onder@app.getbetekent: hoogstens vijf verzoeken per minuut per computer. Hij moet eronder staan; andersom telt hij niets. - Regel 14:
Door de limiter heeft de functie nu
request: Requestnodig. Uit dat verzoek haaltslowapihet adres om te tellen.
Het endpoint zelf doet niets anders dan eerst: de limiet staat tussen
@app.get en de functie, als een portier voor de deur.
Doe de test opnieuw
Zet in test_verzoeken.py de twee getallen uit les 4 weer op 20.
Start je server opnieuw en draai het script.
Hoeveel het script ook stuurt, je server doet nog maar vijf keer echt werk per minuut. Een bezoeker die één keer je pagina opvraagt, merkt er niets van.
Draai je het script meteen nog een keer, dan krijg je twintig keer 429: de
minuut is nog niet om, en je hebt je vijf al gehad. Sla je main.py op, dan
begint de telling wel opnieuw: fastapi dev start de server dan opnieuw, en
die vergeet wat hij geteld had.
Waar de controle zit
De limiet telt vóór je functie draait. Een geweigerd verzoek kost je server daardoor bijna geen werk.
Browser: Een script stuurt veel verzoeken. Veel meer dan een mens die een pagina leest. · Zelf twintig verzoeken
Hier zit de controleServer: slowapi telt per computer. Vóór je functie draait, met @limiter.limit direct onder @app.get. · Een limiet met slowapi
Server: Binnen de grens: je functie draait. Zoals altijd, en het antwoord gaat terug.
Browser: Over de grens: een 429. Je functie draait niet. Met headers_enabled staat erbij wanneer het weer mag. · Wat de 429 vertelt
Er gaat iets mis
De server start niet
Je server start niet, en in de terminal eindigt de foutmelding met:
Exception: No "request" or "websocket" argument on function "<function root at 0x…>"
Oorzaak: je endpoint heeft geen request: Request als parameter, terwijl
er wel @limiter.limit(...) op staat. slowapi controleert dat al bij het
starten: zonder request weet hij niet van wie een verzoek komt. Het getal
achter 0x verschilt per keer.
Oplossing: zet request: Request tussen de haakjes van je functie:
# FOUT
@app.get("/")
@limiter.limit("5/minute")
async def root():
return {"bericht": "Hallo wereld!"}
# GOED
@app.get("/")
@limiter.limit("5/minute")
async def root(request: Request):
return {"bericht": "Hallo wereld!"}
Twintig keer 200
Je server start zonder foutmelding, maar test_verzoeken.py geeft twintig keer
status 200, en aan het eind Gelukt: 20 van de 20.
Oorzaak: @limiter.limit staat boven @app.get in plaats van eronder.
FastAPI legt dan de functie zonder limiet vast als endpoint, en slowapi telt
niets. Je krijgt geen foutmelding; de limiet werkt stil niet.
Oplossing: zet @app.get bovenaan, en @limiter.limit direct eronder:
# FOUT
@limiter.limit("5/minute")
@app.get("/")
async def root(request: Request):
return {"bericht": "Hallo wereld!"}
# GOED
@app.get("/")
@limiter.limit("5/minute")
async def root(request: Request):
return {"bericht": "Hallo wereld!"}
Sla main.py op en draai test_verzoeken.py opnieuw.
Opdrachten
Opdracht 1: Predict - Wat zie je?
Je hebt deze limiet op een endpoint gezet:
@app.get("/tijd")
@limiter.limit("3/minute")
async def tijd(request: Request):
return {"bericht": "Het is nu laat"}
Je vraagt /tijd vier keer snel achter elkaar op.
Vraag: welke statuscodes krijg je terug?
Tip
De limiet is drie per minuut. Tel hoeveel verzoeken er antwoord krijgen voordat de grens bereikt is.
Antwoord
200
200
200
429
De eerste drie passen binnen de limiet, het vierde weigert de server met 429 Too Many Requests.
Opdracht 2: Modify - Tien per minuut
Zet een limiet van 10/minute op je startpagina en sla main.py op. Wachten
hoeft niet: bij het opslaan start fastapi dev je server opnieuw, en daarmee
begint de telling weer bij nul. Draai test_verzoeken.py. Hoeveel verzoeken
krijgen 200, en hoeveel 429?
Antwoord
Tien krijgen 200, de andere tien 429. Aan het eind staat er
Gelukt: 10 van de 20.
Zet de limiet daarna terug op 5/minute: les 6 rekent met vijf.
Opdracht 3: Investigate - Een strengere grens
Verander de limiet in "2/minute", sla op en draai het script opnieuw. Waarom zou een
limiet van twee per minuut op een echte site geen goed idee zijn?
Tip
Denk aan een bezoeker die na het lezen van je startpagina op een link klikt en terugkomt.
Antwoord
Nu krijgen maar twee verzoeken 200 en de rest 429. Op een echte site is dat
te streng: een gewone bezoeker die een paar pagina's bekijkt of even
terugklikt, zit zo aan twee verzoeken, en dan weigert je server hem. Een limiet
moet hoog genoeg zijn voor een mens, en laag genoeg om een script te stoppen.
Zet de limiet daarna terug op 5/minute, zoals in de stand hieronder: les 6
rekent met vijf.
Zo ziet je main.py er nu uit
from fastapi import FastAPI, Request
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)
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):
return {"bericht": "Hallo wereld!"}
De startpagina heeft nu een limiet van vijf per minuut.
Door naar les 6: wat de 429 vertelt.