Ga naar hoofdinhoud

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.

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

    Response komt uit fastapi, net als Request, en komt daarom in deze regel erbij.

  2. Regel 6:

    headers_enabled=True: stuur bij elk antwoord mee hoe het met de limiet staat.

  3. Regel 14:

    Het endpoint krijgt een parameter response: Response. Op dat antwoord zet slowapi de 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")
  1. Regel 7:

    x-ratelimit-remaining: hoeveel verzoeken er deze minuut nog over zijn.

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

Controlevraag
Je script krijgt een 200 met x-ratelimit-remaining 2 en retry-after 40. Wat doet een eerlijk script daarna?
Kies wat jij denkt. Bij het goede antwoord zie je de uitleg.

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:

  • slowapi is geïnstalleerd in de virtual environment van je project.
  • Bovenaan main.py staan de imports en de limiter, en onder app = FastAPI() de twee regels die hem aan je app hangen.
  • Het endpoint dat opslaat, heeft @limiter.limit onder @app.post.
  • request: Request staat 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.