Search Topics

Search across all FastAPI topics

GitHub

Headers & Cookies

Core Concept

Beyond path and query parameters, FastAPI gives you typed access to request headers and cookies using Header() and Cookie() — with the same validation and auto-documentation you already know.

You set a cookie in your response, but the frontend JavaScript can't read it. document.cookie returns empty. The cookie is there in DevTools, so where did it go?

terminal
# FastAPI backend
response.set_cookie(key="session", value="abc123", httponly=True)

# Frontend JavaScript
console.log(document.cookie)  // → "" (empty!)

# But Chrome DevTools → Application → Cookies shows:
# session = abc123  ✓ HttpOnly

Question

The cookie is there — you can see it in DevTools. But JavaScript can't read it. That's not a bug. You set httponly=True, which deliberately hides the cookie from JavaScript. It's a security feature that protects against XSS attacks. The browser still sends it with every request — your backend just made it invisible to scripts.

Headers vs. Cookies: When to Use Which

Both headers and cookies carry data between client and server. But they serve different purposes and have different security characteristics.

Headers

Per-request metadata (auth tokens, content type)

Cookies

Persistent state (sessions, preferences)

Headers are set explicitly by your code on every request. Cookies are set once by the server and automatically sent by the browser on every subsequent request. That's the key difference.

Headers vs cookies

What you just learned

Headers carry per-request metadata — the client sets them explicitly

Cookies persist across requests — the browser sends them automatically

httponly=True hides cookies from JavaScript (security feature, not a bug)

Reading Request Headers

Declare a parameter with Header() to extract values from request headers. It works just like Query() and Path() — same validation, same docs.

main.py
from fastapi import FastAPI, Header

app = FastAPI()

@app.get("/items")
async def read_items(user_agent: str = Header()):
    return {"User-Agent": user_agent}

# Optional header with a default
@app.get("/protected")
async def protected_route(x_token: str | None = Header(default=None)):
    if x_token is None:
        return {"message": "No token provided"}
    return {"token": x_token}

The Underscore-to-Hyphen Magic

Insight

Here's a small thing that trips people up. HTTP headers use hyphens (X-Token), but Python variables can't have hyphens. FastAPI automatically converts underscores to hyphens, so x_token in your code maps to the X-Token header.

main.py
from fastapi import FastAPI, Header

app = FastAPI()

# x_custom_header → X-Custom-Header  (auto-converted)
@app.get("/info")
async def get_info(
    x_custom_header: str = Header(),
    accept_language: str = Header(default="en"),
):
    return {
        "custom_header": x_custom_header,
        "language": accept_language,
    }

# Disable auto-conversion if needed
@app.get("/raw")
async def raw_header(
    strange_header: str = Header(convert_underscores=False),
):
    # Expects literally "strange_header" as the header name
    return {"header": strange_header}

Request headers

What you just learned

Header() extracts typed values from request headers

Python underscores auto-convert to HTTP hyphens (x_token → X-Token)

Optional headers use Header(default=None) or Header(default='value')

Reading Cookies from Requests

Reading cookies follows the exact same pattern as headers. Use Cookie() instead of Header() — that's the only difference.

main.py
from fastapi import FastAPI, Cookie

app = FastAPI()

@app.get("/me")
async def read_user(
    session_id: str | None = Cookie(default=None),
):
    if session_id is None:
        return {"message": "No session"}
    return {"session_id": session_id}

# Multiple cookies
@app.get("/preferences")
async def get_preferences(
    theme: str = Cookie(default="light"),
    language: str = Cookie(default="en"),
):
    return {"theme": theme, "language": language}

Setting Response Headers

Need to send custom headers back to the client? Inject the Response object and set whatever headers you need.

main.py
from fastapi import FastAPI, Response

app = FastAPI()

@app.get("/items")
async def read_items(response: Response):
    response.headers["X-Custom-Header"] = "my-value"
    response.headers["X-Request-ID"] = "abc-123"
    return {"message": "Check the response headers"}

# Or return a Response directly
from fastapi.responses import JSONResponse

@app.get("/custom")
async def custom_response():
    content = {"message": "Hello"}
    headers = {
        "X-Custom-Header": "my-value",
        "X-Process-Time": "0.042",
    }
    return JSONResponse(content=content, headers=headers)

Cookies and response headers

What you just learned

Cookie() reads cookies with the same pattern as Header()

Inject Response to set custom headers on outgoing responses

JSONResponse gives you full control over status, headers, and body

The HttpOnly Cookie Mystery

You set a session cookie but your frontend JavaScript can't read it. The cookie exists in DevTools but document.cookie is empty.

Broken code
main.py
from fastapi import FastAPI, Response

app = FastAPI()

@app.post("/login")
async def login(response: Response):
    response.set_cookie(
        key="session",
        value="abc123",
        httponly=True,  # ← This is the cause
    )
    return {"message": "Logged in"}

Go Deeper: Cookie Security Flags

Each flag in set_cookie() protects against a specific attack. Understanding what each does is critical for secure auth.

main.py
response.set_cookie(
    key="session_id",
    value="abc123",

    # httponly=True → JavaScript CANNOT read this cookie
    # Protects against XSS (cross-site scripting)
    # An attacker injecting JS can't steal the token
    httponly=True,

    # secure=True → Cookie only sent over HTTPS
    # Prevents interception on insecure networks
    # Always use in production!
    secure=True,

    # samesite="lax" → Cookie not sent on cross-site requests
    # Protects against CSRF (cross-site request forgery)
    # "strict" = never cross-site, "lax" = safe navigation only
    samesite="lax",

    # max_age=3600 → Cookie expires in 1 hour
    # Without this, cookie dies when browser closes (session cookie)
    # Short expiry = less damage if stolen
    max_age=3600,

    # domain → which domains receive this cookie
    # path → which URL paths receive this cookie
    # Omit both for maximum restriction (current domain + path only)
)

Cookie security

What you just learned

httponly=True prevents XSS attacks from stealing session tokens

secure=True ensures cookies only travel over HTTPS

samesite prevents CSRF — use 'lax' for most cases, 'strict' for sensitive actions

max_age controls cookie lifetime — shorter is safer for auth tokens

Where Should You Store Tokens?

Not all storage is created equal. Compare HttpOnly cookies, localStorage, sessionStorage, and in-memory storage — see what's vulnerable to XSS, what survives a refresh, and what's actually safe.

Think about it...

What's the difference between a Header() parameter and reading request.headers directly?

Hint: Think about what happens to header names with hyphens, and how the parameter shows up in /docs.

Key Points

Header()

Declare typed header parameters with validation and defaults

Cookie()

Read cookies from requests with the same pattern as headers

Auto-Conversion

Python underscores become HTTP hyphens automatically

httponly=True

Hides cookie from JavaScript — the #1 defense against XSS token theft

secure + samesite

HTTPS-only transport and CSRF protection in two flags

Storage Matters

HttpOnly cookies > memory > sessionStorage > localStorage for auth tokens