The parts of a request that are neither path, query nor body - and what each is properly for.
Overview
Reading a header
Headers and cookies carry information about a request that is neither part of the path, nor the query string, nor the body. A header might say what format the client wants, which browser it is, or who it claims to be. A cookie is a small value the browser stores and sends back on every later request to the same site.
FastAPI reads both with the same annotation approach you use elsewhere. You declare an argument, mark it as a header or a cookie, and the framework extracts and converts the value. One detail surprises people: header names use hyphens, while Python names cannot. FastAPI converts underscores to hyphens for you.
This article starts with reading a single header and what the conversion rules are. It then explains what headers are for, so you can tell which information belongs there rather than in a query string. After that it covers cookies, how a browser sends them, and how you read one.
The article also looks at the other direction. Your response can set headers and cookies too, which is how a server hands a session or a caching hint to the client. You will see the proper way to do this without building a full response by hand.
It closes with authentication. Checking a token in a header is something many endpoints share, so it belongs in a dependency instead of being repeated in every handler.
def whoami(x_token: str = Header(default=None)):
FastAPI converts underscores to hyphens. So x_token reads the x-token header. That conversion exists because most header names hold hyphens and none of them are valid Python identifiers.
If you need the literal name. A header that truly holds an underscore. Header(convert_underscores=False) turns it off.
Header names are case-insensitive in HTTP. The framework norms them. A client sending X-Token, x-token or X-TOKEN all reach the same param. That is worth knowing mainly so you don't write code trying to handle the variants.
all from the query-param module applies: a default makes it optional, no default makes it needed. Header() carries the same constraints and metadata. This Appear in the schema.
Worth knowing
Header() reads one header. Underscores in the parameter name become hyphens, because x-token is not a valid identifier.
Header names are case-insensitive per HTTP, so the case a client sends does not matter.
A header with no default is required, and its 422 carries loc[0] == "header".
Cookie() reads one cookie by name — a header underneath, parsed for you.
Declaring a Response parameter lets you set outgoing headers and cookies without returning a Response object.
Use httponly=True for a session cookie so page scripts cannot read it, and set samesite deliberately.
Headers and Cookies
The parts of a request that are neither path, query nor body. What each is properly for.
Header() reads one header
Underscores in the parameter name become hyphens automatically, because x-token is not a valid Python identifier.
example_01.pyFastAPI
from typing import Optional
from fastapi import FastAPI, Header
app = FastAPI()
@app.get("/whoami")
def whoami(x_token: Optional[str] = Header(default=None),
user_agent: Optional[str] = Header(default=None)):
return {"x_token": x_token, "user_agent": user_agent}
c = TestClient(app)
print("none :", c.get("/whoami").json())
print("supplied:", c.request("GET", "/whoami", headers={
"x-token": "abc123", "user-agent": "vizlearn-test/1.0"}).json())
print()
print("x_token became the header x-token, with no configuration.")
Output
Header names are case-insensitive
HTTP says so, and the client normalises them. Your parameter matches whatever case arrived.
example_02.pyFastAPI
from typing import Optional
from fastapi import FastAPI, Header
app = FastAPI()
@app.get("/h")
def h(x_token: Optional[str] = Header(default=None)):
return {"token": x_token}
c = TestClient(app)
for name in ["x-token", "X-Token", "X-TOKEN"]:
print("%-9s ->" % name, c.request("GET", "/h", headers={name: "abc"}).json())
Output
Required headers, and the 422
A header with no default is required, and its failure reports header as the source.
What doesn't belong is data. A filter, an identifier, a search term. Those are query params or a body. A header is invisible in a URL. Anything in one can't be bookmarked, linked or shared. Callers won't think to look for it.
The X- prefix for custom headers was formally deprecated years ago and remains near-universal . Either rule is fine. Agreement matters more than which.
Cookies
Cookie() reads one by name. Underneath it's the Cookie header, parsed into pairs for you.
Cookies are worth being careful with. They're sent on its own by browsers on each matching request. Which is what makes them convenient for sessions and what makes them a CSRF vector.
Three flags matter when setting one:
**httponly=True** stops page JavaScript reading it. For a session identifier this is close to mandatory. Without it, any script that gets injected can read the session.
**samesite** controls whether the browser sends it on cross-site requests. "lax" is a fair default and blocks the most common CSRF shapes. "strict" is safer and breaks arriving from an external link. "none" needs secure=True.
**secure=True** sends it only over HTTPS. In production it should always be set.
For an API consumed by a split front end, tokens in an Authorization header are often the simpler choice. They aren't sent on its own and so CSRF doesn't arise. Cookies earn their place when the browser is the client and you want the browser's session handling.
Setting things on the way out
Declaring a Response param gives you the response object FastAPI is about to send:
You still return your normal value. The response model still applies. That is the handy part. You get header control without giving up output and docs the way returning a Response object does.
Auth belongs in a dependency
Reading an Authorization header in each handler that needs it works and doesn't scale. The same four lines end up in thirty functions, and the thirty-first forgets them.
That is what dependencies are for. They're the next tier. A dependency reads the header, validates the token, raises 401 if it's wrong, and returns the user. And each endpoint that needs authentication declares one param.
Worth knowing now so you don't build the habit of doing it by hand.
What not to do
Don't put secrets in a URL. They end up in browser history, server logs, proxy logs and Referer headers. That is the arg for an Authorization header over an api_key query param, and it's a strong one.
Don't trust a client-supplied header for identity.X-User-Id from a caller is a claim, not a fact. Behind a trusted proxy that sets it, it's a fact. But only if you've confirmed the proxy strips what the client sent.
Don't log headers indiscriminately.Authorization and Cookie are just the two you least want in a log file, and they're in each request.
Mistakes people make with Headers and Cookies
Putting data in a header. A filter or an identifier in a header can't be bookmarked, linked or shared. No caller will think to look for it. Headers carry metadata about the request. The URL and body carry its subject.
Trusting a client-supplied identity header.X-User-Id from a caller is a claim. It's a fact only behind a proxy that sets it *and* strips what the client sent - and you've to have checked the second half.
A secret in a query param. It lands in browser history, server logs, proxy logs and Referer headers. That is the arg for Authorization over ?api_key=, and it's a strong one.
Logging headers wholesale.Authorization and Cookie are the two you least want persisted, and they're in each request.
**A session cookie without httponly.** Any injected script can then read the session.
Reading auth headers in each handler. The same four lines in thirty functions. The thirty-first forgets them. That is what a dependency is for.
Cookies or tokens
Worth being clear, because it's a choice each API makes once.
Cookies are sent on its own by the browser on each matching request. Convenient for a server-rendered app. The reason CSRF exists - which samesite mitigates and a token approach avoids fully.
Bearer tokens in an Authorization header aren't sent on its own. CSRF doesn't arise. The client has to store the token one spot. localStorage is readable by scripts. This Trades one risk for another.
For an API consumed by a split front end, tokens are often simpler. For a browser-first use where the server manages the session, cookies with httponly, secure and samesite set are a good answer and a well-understood one.
Neither is universally right. What is universally wrong is choosing without noticing there was a choice.
Content negotiation, briefly
Two headers decide what format a request and response are in. FastAPI handles both mostly invisibly.
Content-Type on the request says what the body is. It's how the framework knows whether to parse JSON or a form. This Is why declaring a model and a Form() field together is a clash - they imply other values for one header.
Accept on the request says what the client would like back. FastAPI doesn't negotiate on it by default. An endpoint returns JSON even so. If you need to serve more than one form, you read the header yourself and return a other response class.
That is a deliberate simplification rather than an omission. Genuine content negotiation is rarer than it looks. An API that always returns JSON is easier to consume than one whose response shape depends on a header the caller may not have set.
Caching headers
Worth knowing they exist, because a small amount of effort here drops a large amount of traffic.
ETag and If-None-Match let a client ask "has this changed?" and receive a 304 with no body when it hasn't. Cache-Control tells intermediaries how long a response may be reused.
Neither is auto. Both are set through a Response param. Both only make sense on GET - which is another reason the safe-method rule matters. A cached response to a mutating request would be a genuine problem.
For a read-heavy API serving data that changes rarely, an ETag on the costly endpoints is often the cheapest speed work on hand.
Trusting the proxy
One more note, because it catches people deploying for the first time.
Behind a load balancer or reverse proxy, the client address your app sees is the proxy's, not the caller's. The real one arrives in X-Forwarded-For, and the scheme in X-Forwarded-Proto.
Those are headers like any other. This Means a direct caller can set them to anything. They're trustworthy only if the proxy overwrites rather than appends. Only if nothing can reach your app without going through it.
Getting that wrong is how rate limiting by IP turns rate limiting by what the attacker chose to send.
Where to go after Headers and Cookies
The two content types a browser form really sends. This Are neither JSON nor query strings: form data and file uploads.
A note on CORS
Headers are also where the browser's cross-origin rules live. It's worth knowing where the boundary is.
When a page on one origin calls an API on another, the browser decides whether the response may be read. That choice is made from response headers - Access-Control-Allow-Origin and its relatives - and for anything beyond a simple request the browser first sends an OPTIONS preflight asking what is permitted.
The key part: CORS is enforced *by the browser*, for the browser's gain. It isn't a security control on your server. A non-browser client skips it fully. A permissive CORS policy doesn't expose an API that was else protected. A strict one doesn't protect an API that has no authentication.
FastAPI configures this with CORSMiddleware. This Is the middleware module's subject. The habit to avoid is reaching for allow_origins=["*"] because a thing didn't work - it often does make the error go away. It also means any page anywhere can call your API with what credentials the browser holds.
Where headers fit in the request
It helps to hold the four sources in one picture, now that all of them have appeared.
The path finds a resource and is always needed. The query string shows a view of it and is often optional. The body carries the subject of a write. Headers and cookies carry all about the exchange that isn't about the resource at all - who is asking, in what format, on behalf of which session, with what caching state.
FastAPI reads all four from one function signature, deciding by where a name appears and what marks it. That uniformity is the framework's main part. It's why a handler taking a path param, two query filters, a body and an API key still reads as an plain Python function.
The corollary is that putting a value in the wrong place is easy and makes a working endpoint that is awkward to use. An identifier in a header, a session token in the query string, a filter in the body of a GET - each works. Each will confuse whoever integrates with it.
Questions people ask about Headers and Cookies
How do I read a request header? Declare a parameter with Header(), for example user_agent: str | None = Header(default=None). FastAPI converts underscores to hyphens for the lookup.
Why is my header name lowercase in Python? HTTP header names are case-insensitive, and FastAPI maps user_agent to User-Agent. You can disable the conversion with convert_underscores=False.
How do I read a cookie? Declare a parameter with Cookie(), for example session_id: str | None = Cookie(default=None).
How do I set a cookie or header on the response? Take a Response parameter and call response.set_cookie(...) or set response.headers["X-Name"]. FastAPI merges it into the final response.
Where should authentication headers be read? In a dependency, so every protected endpoint reuses one check instead of repeating the header logic.
Cookies or tokens in headers? Cookies suit browser apps and bring CSRF concerns. Bearer tokens in the Authorization header suit APIs called from scripts and mobile apps.
Headers and Cookies: summary
Header() reads one header, converting underscores to hyphens. Cookie() reads one cookie. Both behave like query params else. A default makes them optional, constraints and descriptions reach the schema. A failure reports header or cookie as the source.
Headers carry metadata about a request - authentication, content negotiation, caching, tracing. Data belongs in the URL or the body. Here It can be seen and shared.
Set outgoing headers and cookies through a Response param. This Keeps your response model. Use httponly, secure and samesite on anything that finds a session. Move authentication into a dependency before the fourth endpoint needs it.
Check yourself
0 of 4
Answer without scrolling back up.
Why does `x_token: str = Header()` read the `x-token` header?
Most header names contain hyphens and none are valid Python identifiers, so the conversion is automatic. `convert_underscores=False` turns it off.
What belongs in a header rather than a query parameter?
Headers carry metadata about the request. Data belongs where it can be seen, bookmarked and linked - the URL or the body.
Why set `httponly=True` on a session cookie?
It keeps the value out of reach of scripts. CSRF is a different problem, addressed by `samesite`.
Where should reading an `Authorization` header live?
Doing it per handler means the same four lines in thirty functions and the thirty-first forgetting. A dependency does it once and each endpoint declares one parameter.
Cheat sheet
Headers and Cookies
Read a request header and a cookie inside a FastAPI endpoint, set a cookie on the response, and see what each transport is properly for.
Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.