HTTP requests carry more than just a URL and body โ headers provide metadata about the request (Content-Type, Authorization, User-Agent, Accept-Language) and cookies carry session state. FastAPI provides the Header and Cookie parameter functions to declare these as typed, validated function arguments just like path and query parameters. For cases where you need access to the full request โ including raw headers, client IP, URL, method, or streaming body โ FastAPI exposes the Starlette Request object as an injectable dependency.
Request Headers
from fastapi import FastAPI, Header
from typing import Annotated
app = FastAPI()
# โโ Declare a header parameter โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
# FastAPI automatically converts header names:
# HTTP header "X-User-Id" โ function param "x_user_id" (hyphens to underscores)
# HTTP header "User-Agent" โ function param "user_agent"
@app.get("/posts")
def list_posts(
user_agent: Annotated[str | None, Header()] = None,
accept_lang: Annotated[str | None, Header(alias="accept-language")] = None,
):
return {"user_agent": user_agent, "language": accept_lang}
# โโ Custom application headers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
@app.get("/internal/stats")
def get_stats(
x_api_key: Annotated[str, Header(description="Internal API key")],
x_client_id: Annotated[str | None, Header()] = None,
):
if x_api_key != "secret-internal-key":
from fastapi import HTTPException
raise HTTPException(403, "Invalid API key")
return {"client": x_client_id, "stats": {}}
X-User-Id becomes x_user_id, Content-Type becomes content_type. HTTP headers are case-insensitive (as per the HTTP spec), so FastAPI lowercases them as well. If you need to use the exact header name (e.g., for headers with non-standard casing), use the alias parameter: Header(alias="X-Custom-Header").Authorization: Bearer token is the standard for JWT auth, X-API-Key: value for API keys. FastAPI’s Header parameter handles these cleanly with type validation and documentation in Swagger UI.x_request_timeout: int), but this only works if the header value is a valid integer string. For headers that contain structured data (like JWT tokens), always declare them as str and parse the content in your handler or dependency.Request Cookies
from fastapi import FastAPI, Cookie
from typing import Annotated
app = FastAPI()
@app.get("/profile")
def get_profile(
session_id: Annotated[str | None, Cookie()] = None,
theme: Annotated[str, Cookie()] = "light",
):
if session_id is None:
from fastapi import HTTPException
raise HTTPException(401, "Not authenticated")
return {"session": session_id, "theme": theme}
# Cookie names map directly โ no hyphen-to-underscore conversion
# Cookie: session_id=abc123; theme=dark
The Request Object
from fastapi import FastAPI, Request
import json
app = FastAPI()
@app.get("/debug")
async def debug_request(request: Request):
return {
"method": request.method,
"url": str(request.url),
"path": request.url.path,
"query_string": str(request.query_params),
"client_ip": request.client.host if request.client else None,
"headers": dict(request.headers),
"cookies": dict(request.cookies),
}
# โโ Read raw request body โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
@app.post("/webhooks/raw")
async def receive_webhook(request: Request):
# Read raw bytes โ useful for HMAC signature verification
body = await request.body()
# Verify signature (e.g. GitHub webhook)
import hmac, hashlib
signature = request.headers.get("X-Hub-Signature-256", "")
secret = b"your_webhook_secret"
expected = "sha256=" + hmac.new(secret, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected):
from fastapi import HTTPException
raise HTTPException(401, "Invalid webhook signature")
payload = json.loads(body)
return {"received": True, "event": request.headers.get("X-GitHub-Event")}
Common Mistakes
Mistake 1 โ Header name with wrong underscore/hyphen convention
โ Wrong โ expecting HTTP hyphen in Python parameter name:
def handler(x-api-key: str = Header()): # SyntaxError โ hyphens invalid in Python names
โ Correct โ FastAPI converts automatically:
def handler(x_api_key: Annotated[str, Header()]): # โ x_api_key โ X-Api-Key header
Mistake 2 โ Reading raw body after Pydantic body model (body already consumed)
โ Wrong โ both a Pydantic body and raw body read:
async def handler(data: MyModel, request: Request):
body = await request.body() # empty! Pydantic already consumed it
โ Correct โ use only one body reading method. For HMAC verification, read raw body and parse JSON yourself.
Mistake 3 โ Not handling missing optional headers
โ Wrong โ required header causes 422 if not sent:
def handler(x_request_id: Annotated[str, Header()]): # required! 422 if missing
โ Correct โ make it optional with a default:
def handler(x_request_id: Annotated[str | None, Header()] = None): # โ optional
Quick Reference
| Feature | Code |
|---|---|
| Required header | key: Annotated[str, Header()] |
| Optional header | key: Annotated[str | None, Header()] = None |
| Header with alias | Header(alias="X-Custom-Name") |
| Cookie | session: Annotated[str | None, Cookie()] = None |
| Full request | request: Request parameter |
| Client IP | request.client.host |
| Raw body | await request.body() |
| All headers | dict(request.headers) |