Unit 04.01: Query parameters, defaults and optionality
Query parameters need defaults and bounds, and the bound is the one people leave out.
Declared, defaulted, constrained
A listing endpoint with a defaulted status and a bounded limit.
The code calls it three ways, including one over the bound.
from fastapi import FastAPI, Query
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/invoices")
def list_invoices(status: str = "open",
limit: int = Query(default=50, ge=1, le=200)) -> dict:
return {"status": status, "limit": limit}
client = TestClient(app)
for url in ["/invoices", "/invoices?status=paid", "/invoices?limit=500"]:
r = client.get(url)
print(f"{url:26} -> {r.status_code} {r.json() if r.status_code == 200 else ''}")
print("\nThe bounded limit is the important one: without `le=200` a caller can")
print("ask for a million rows and your database will try to provide them.")
The bounded limit is the important one. Without le=200 a caller can ask for a million rows, your database will try to provide them, and the endpoint becomes a denial-of-service vector that you built yourself.
The bound is also in the published schema, so a caller sees the maximum rather than discovering it by being rejected.
The mistake this prevents
The mistake is adding pagination without a maximum, because the default is sensible. The default protects the caller who does not specify; the maximum protects you from the one who does.
Takeaway
Give every query parameter a default and a bound. An unbounded limit is a denial-of-service vector in your own code.
