Search Topics
Search across all FastAPI topics
Headers & Cookies
Core ConceptBeyond 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?
# 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.
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.
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.
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.
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.
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.
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