FastAPI Essentials

Course Content

FastAPI Essentials

1 sections · 32 lessons

When would you use HTTP directly instead of FastAPI’s dependency injection system?


Verifying a signed webhookRaw bytes from request.body()• Exactly what the sender signed• HMAC matches: request accepted• Parse with Pydantic after verifying• Tampered body gets 401Re-encoded Pydantic model• Spaces and key order change• HMAC differs: valid webhook fails• Team switches verification off• Anyone can post fake events
A signature covers bytes, not meaning, so the check has to run before anything parses and re-encodes the body.

What you need to know

The question is about choosing between two levels:

  • High level: typed parameters and Depends. FastAPI parses, validates, documents and injects.
  • Low level: the raw Request (headers, await request.body(), request.stream()) or middleware that sees raw ASGI traffic. You get exactly what arrived, with no help.

Case 1: webhook signatures

A provider — a payment gateway, or an LLM platform telling you a fine-tuning job finished — signs the raw request body with a shared secret. You must compute the same signature over the same bytes.

Python
import hashlib, hmacfrom fastapi import FastAPI, HTTPException, Requestfrom pydantic import BaseModelSECRET = b"whsec_demo"app = FastAPI()class JobDone(BaseModel):    job_id: str    status: strdef sign(raw: bytes) -> str:    return hmac.new(SECRET, raw, hashlib.sha256).hexdigest()@app.post("/webhooks/fine-tune")async def fine_tune_done(request: Request):    raw = await request.body()                           # the exact bytes that were signed    if not hmac.compare_digest(sign(raw), request.headers.get("x-signature", "")):        raise HTTPException(401, "Bad signature")    event = JobDone.model_validate_json(raw)             # validate only after verifying    return {"received": event.job_id}

Real results, plus a naive version that declares event: JobDone and signs event.model_dump_json() instead:

Text
correct signature           -> {'received': 'ft-91'}body changed, same sig      -> 401naive version (re-encoded)  -> {'signature_ok': False}

The naive version fails on a perfectly valid webhook. The sender wrote {"job_id": "ft-91", ...} with spaces; Pydantic re-encodes it as {"job_id":"ft-91",...} without them. Different bytes, different signature. Raw bytes are the only correct input here. Note also hmac.compare_digest, which compares in constant time so attackers cannot guess the signature from timing.

Other cases

SituationUseWhy not a typed parameter
Huge upload you want to stream to storageasync for chunk in request.stream()A body model would load it all first
Proxy to another service unchangedRaw body and headersParsing and re-encoding may change them
Request id, timing, access logs for all requestsMiddlewareRuns even for 404s, which never reach a dependency
Calling another serviceA shared httpx.AsyncClient (injected)DI decides how you get the client, not how you call

When not to go raw

Do not wrap trivial values in Depends for its own sake, and do not read request.json() by hand when a Pydantic model would do. Going raw removes validation, the typed object and the endpoint's OpenAPI description. Keep raw handling at the boundary and hand a validated model to the rest of the code.

A real-life example

A SaaS company fine-tunes customer models on an LLM platform, which calls a webhook when each job finishes. The first version declared event: JobDone and verified the signature over event.model_dump_json(). Every webhook failed verification, so the team "temporarily" turned verification off. For three weeks anyone who knew the URL could mark a job as finished.

The fix was the raw-body version above. The team also added a test that posts a captured real webhook, with its original bytes and signature, so any future refactor that breaks verification fails CI instead of being switched off.

Follow-up questions to expect

  • "Can a dependency read the raw body?" — Yes. A dependency can take request: Request and await request.body(); Starlette caches the body, so the route can still read it afterwards.
  • "Middleware or dependency for auth?" — Usually a dependency: it is per-route, typed, documented and testable. Middleware suits rules that truly apply to every path.
  • "Why validate after verifying?" — So unauthenticated input never reaches your parsing code, and so the bytes you verified are the bytes you use.