Ga naar hoofdinhoud

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!"}
  1. Regel 1-4:

    Request komt erbij in de regel uit fastapi, want de limiet moet het verzoek zien. De andere drie regels komen van slowapi.

  2. Regel 6:

    De Limiter telt de verzoeken. get_remote_address zegt: tel per computer, aan de hand van zijn adres. Voor jouw script is dat 127.0.0.1.

  3. 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.

  4. Regel 13:

    @limiter.limit("5/minute") direct onder @app.get betekent: hoogstens vijf verzoeken per minuut per computer. Hij moet eronder staan; andersom telt hij niets.

  5. Regel 14:

    Door de limiter heeft de functie nu request: Request nodig. Uit dat verzoek haalt slowapi het 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.

Waar de controle zit: te veel verzoeken
  1. Browser: Een script stuurt veel verzoeken. Veel meer dan een mens die een pagina leest. · Zelf twintig verzoeken

  2. Hier zit de controleServer: slowapi telt per computer. Vóór je functie draait, met @limiter.limit direct onder @app.get. · Een limiet met slowapi

  3. Server: Binnen de grens: je functie draait. Zoals altijd, en het antwoord gaat terug.

  4. Browser: Over de grens: een 429. Je functie draait niet. Met headers_enabled staat erbij wanneer het weer mag. · Wat de 429 vertelt

Controlevraag
Waarom kost een verzoek dat slowapi weigert je server bijna geen werk?
Kies wat jij denkt. Bij het goede antwoord zie je de uitleg.

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.