Request Headers, Cookies and the Request Object

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": {}}
Note: FastAPI converts HTTP header names to Python-friendly names by replacing hyphens with underscores โ€” 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").
Tip: For production APIs, avoid putting sensitive values like API keys and authentication tokens in query parameters (they appear in server logs and browser history). Use HTTP headers instead โ€” 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.
Warning: Header values are always strings in HTTP. FastAPI will attempt to coerce header values to the declared type (e.g., 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)

🧠 Test Yourself

A client sends the header X-Request-Id: abc123. Your function declares x_request_id: Annotated[str | None, Header()] = None. What value does x_request_id have?