Headers are a fundamental part of HTTP requests and responses, carrying metadata about the message body, client capabilities, server information, and more. sillo provides comprehensive tools for working with headers in both requests and responses.
Headers
Section titled “Headers”Access incoming request headers through the request.headers property, which provides a case-insensitive dictionary-like interface:
from sillo import silloAppapp = silloApp()@app.get("/")async def show_headers(request, response): user_agent = request.headers.get("user-agent") accept_language = request.headers.get("accept-language") return { "user_agent": user_agent, "accept_language": accept_language }Common Request Headers
Section titled “Common Request Headers”| Header | Description | Example |
|---|---|---|
Accept | Content types the client can process | application/json, text/html |
Authorization | Credentials for authentication | Bearer xyz123 |
Content-Type | Media type of the request body | application/json |
Cookie | Cookies sent by the client | session_id=abc123 |
User-Agent | Client application information | Mozilla/5.0 |
X-Requested-With | Indicates AJAX request | XMLHttpRequest |
Response Headers
Section titled “Response Headers”Set response headers using the response.set_header() method or by passing a headers dictionary:
from sillo import silloAppapp = silloApp()@app.get("/")async def set_headers(request, response): response.set_header("X-Custom-Header", "Custom Value") response.set_header("Cache-Control", "no-store") return response.text("Hello, World!")Common Response Headers
Section titled “Common Response Headers”| Header | Description | Example |
|---|---|---|
Content-Type | Media type of the response | text/html; charset=utf-8 |
Cache-Control | Caching directives | max-age=3600 |
Set-Cookie | Sets cookies on client | session_id=abc123; Path=/ |
Location | URL for redirects | https://example.com/new |
X-Frame-Options | Clickjacking protection | DENY |
Content-Security-Policy | Security policy | default-src 'self' |
Setting Headers In Middleware
Section titled “Setting Headers In Middleware”In middleware, you need to modify headers after the response has been created by the route handler.
Correct Middleware Pattern
Section titled “Correct Middleware Pattern”Always set headers after calling await call_next():
async def cors_middleware(request, response, call_next): response = await call_next() response.set_header("Access-Control-Allow-Origin", "*") response.set_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE") response.set_header("Access-Control-Allow-Headers", "Content-Type, Authorization") return responseCommon Middleware Use Cases
Section titled “Common Middleware Use Cases”CORS Headers
Section titled “CORS Headers”async def cors_middleware(request, response, call_next): response = await call_next() response.set_header("Access-Control-Allow-Origin", "*") response.set_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") response.set_header("Access-Control-Allow-Headers", "Content-Type, Authorization") response.set_header("Access-Control-Max-Age", "86400") return responseSecurity Headers
Section titled “Security Headers”async def security_middleware(request, response, call_next): response = await call_next() response.set_header("X-Content-Type-Options", "nosniff") response.set_header("X-Frame-Options", "DENY") response.set_header("X-XSS-Protection", "1; mode=block") response.set_header("Strict-Transport-Security", "max-age=31536000; includeSubDomains") return responseRequest ID Tracking
Section titled “Request ID Tracking”async def request_id_middleware(request, response, call_next): request_id = request.headers.get("X-Request-ID", str(uuid.uuid4())) response = await call_next() response.set_header("X-Request-ID", request_id) return responseHeader Manipulation Methods
Section titled “Header Manipulation Methods”sillo provides several methods for working with headers:
Request Header Methods
Section titled “Request Header Methods”request.headers.get(key, default=None)- Get a header valuerequest.headers.items()- Get all headers as key-value pairsrequest.headers.keys()- Get all header namesrequest.headers.values()- Get all header values
Response Header Methods
Section titled “Response Header Methods”response.set_header(key, value, override=False)- Set a headerresponse.remove_header(key)- Remove a headerresponse.has_header(key)- Check if header existsresponse.set_headers(headers_dict, override_all=False)- Set multiple headers
Security Headers Best Practices
Section titled “Security Headers Best Practices”For enhanced security, consider these recommended headers:
async def add_security_headers(request, response, call_next): response.set_headers({ "X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY", "X-XSS-Protection": "1; mode=block", "Referrer-Policy": "strict-origin-when-cross-origin", "Content-Security-Policy": "default-src 'self'; script-src 'self'", "Strict-Transport-Security": "max-age=63072000; includeSubDomains; preload" }) return await call_next()
app.use(add_security_headers)Performance Headers
Section titled “Performance Headers”Optimize client-side caching and resource loading:
async def add_performance_headers(request, response, call_next): if request.path.endswith(('.js', '.css', '.png', '.jpg')): response.set_header("Cache-Control", "public, max-age=31536000, immutable") return await call_next()
app.use(add_performance_headers)Cookie Headers
Section titled “Cookie Headers”Cookies are set via special Set-Cookie headers:
@app.get("/login")async def login(request, response): response.set_cookie( key="session_id", value="abc123", max_age=3600, secure=True, httponly=True, samesite="strict" ) return response.redirect("/dashboard")Conditional Headers
Section titled “Conditional Headers”Handle conditional requests with these headers:
| Header | Purpose | Example |
|---|---|---|
If-Modified-Since | Check if resource changed | Sat, 01 Jan 2022 00:00:00 GMT |
If-None-Match | Check ETag match | "abc123" |
ETag | Resource version identifier | W/"xyz456" |
Last-Modified | Resource modification time | Sat, 01 Jan 2022 00:00:00 GMT |
Restricted Headers
Section titled “Restricted Headers”Some headers are restricted and cannot be modified:
Content-Length(automatically calculated)ConnectionTransfer-EncodingHost
Further Reading
Section titled “Further Reading”Headers are case-insensitive and repeatable
Section titled “Headers are case-insensitive and repeatable”Two properties of HTTP headers cause most of the surprises.
Case does not matter. Content-Type, content-type, and
CONTENT-TYPE are the same header. sillo’s header mapping handles this,
so request.headers["content-type"] works regardless of what the client
sent. Code that iterates raw ASGI headers and compares with == does not.
A header may appear more than once. Set-Cookie, Accept, Via,
and X-Forwarded-For all legitimately repeat, and a mapping lookup gives
you one of them. Where repetition matters, read the multi-value form
rather than indexing.
The one that bites hardest is X-Forwarded-For, which is comma-separated
and repeatable — a chain through three proxies can arrive as one header
with three values, three headers with one each, or any combination. Any
parser that assumes one shape is wrong behind some proxy.
Security headers worth setting
Section titled “Security headers worth setting”These are cheap, apply globally, and each closes a real class of attack.
Strict-Transport-Security tells browsers to use HTTPS for your domain
for a period, closing the downgrade window on the second visit. Start
with a short max-age; the value is hard to shorten once cached.
X-Content-Type-Options: nosniff stops browsers second-guessing your
Content-Type. Without it, a mislabeled upload can be interpreted as
script.
Content-Security-Policy is the strongest control available against XSS
and the hardest to adopt, because a strict policy breaks inline scripts
and styles. Deploy it in report-only mode first and read the reports.
Referrer-Policy: strict-origin-when-cross-origin stops full URLs —
including path segments and query strings, which may carry tokens —
leaking to third parties.
X-Frame-Options or CSP frame-ancestors prevents your pages being
framed, which is what clickjacking requires.
Set them in middleware so they apply to every response, including errors and static files. A security header applied by a decorator is a security header missing from the routes somebody forgot to decorate.
Reading headers safely
Section titled “Reading headers safely”Every header is client-controlled, so the same rules apply as to any other input: validate before use, bound the length, and never interpolate into a log line, a filename, or a downstream request without escaping.
Header injection is the specific hazard when you write a header from
user input. A newline in a value lets the caller append arbitrary
headers, including a second Set-Cookie. Strip control characters from
anything user-supplied before it becomes part of a response.
Content negotiation headers
Section titled “Content negotiation headers”Accept, Accept-Language, and Accept-Encoding are weighted preference
lists, not single values:
Accept: text/html;q=0.9, application/json;q=1.0, */*;q=0.1Parsing by substring match or by taking the first entry gives wrong answers for any client that sends real quality values. Where it matters, parse properly — and where it does not, ignore the header and return one format consistently, which is more honest than negotiating badly.
Anything whose response varies by one of these must set Vary naming
it. See Content Negotiation.
Caching headers
Section titled “Caching headers”Cache-Control is the one that decides whether anything between you and
the client keeps a copy. Its absence does not mean “do not cache” —
intermediaries apply heuristics — so say what you mean.
no-store on anything authenticated. public, max-age=31536000, immutable on content-hashed static assets. private, max-age=0, must-revalidate on per-user pages that should always be checked.
ETag with If-None-Match turns an unchanged large response into a 304
with no body, which is the cheapest win available on any endpoint that is
polled.
The failure mode worth avoiding: a public cache directive on a response
that varies by user. A shared cache stores one user’s data and serves it
to the next, and neither you nor they will notice quickly.
Related
Section titled “Related”- Request Info — what to trust from the request side
- Content Negotiation —
AcceptandVary - Cache — server-side caching, and how HTTP caching complements it
- CORS — the cross-origin headers
- Security — the security header set
- Cookies —
Set-Cookieand its attributes
Setting headers on a response
Section titled “Setting headers on a response”resp = response.json({"ok": True})resp.headers["X-Request-ID"] = request_idresp.headers["Cache-Control"] = "no-store"return respFor anything that should apply to every response — security headers, correlation ids, timing — set it in middleware instead. A header applied by a decorator is a header missing from the routes nobody decorated, and the missing ones are never the routes you would have chosen.
Two values need care when they come from user input. Newlines and carriage returns must be stripped, because a value containing them can inject additional headers. And length should be bounded, since headers travel on every response and some intermediaries reject oversized ones outright.
Reading headers in sillo
Section titled “Reading headers in sillo”content_type = request.headers.get("content-type", "")auth = request.headers.get("authorization")accepts_json = "application/json" in request.headers.get("accept", "")Lookups are case-insensitive, and a missing header returns None rather
than raising — which means a typo in a header name is indistinguishable
from the header being absent. Where a header is required, check
explicitly and return a clear 400 rather than proceeding with None.
For headers that legitimately repeat, index the multi-value view rather
than the mapping. X-Forwarded-For is the case that matters, and
Network helpers covers the parsing it needs.
Custom headers
Section titled “Custom headers”Prefix conventions have changed: X- prefixes were deprecated by
RFC 6648 in 2012, and the modern advice is to use an unprefixed name
scoped to your organisation. In practice X-Request-ID and friends are
so entrenched that consistency within your own API matters more than the
RFC.
What does matter: document every custom header you require or emit, treat them as part of your API contract, and remember that a header a client must send is a header that will be forgotten. Prefer a body field where the choice is genuinely available.
Size limits
Section titled “Size limits”Servers cap total header size — typically 8 KB to 16 KB across all
headers — and exceeding it produces a 431 or a connection reset rather
than a helpful error.
Two things cause it in practice: cookies that have grown, since every
cookie for the domain is sent on every request; and a JWT with too many
claims sitting in an Authorization header. Both are worth measuring
rather than assuming, because the failure appears only for the users who
have accumulated the most state.
Summary
Section titled “Summary”Header names are case-insensitive and some headers legitimately repeat — index the multi-value view where that matters. Set the security header set in middleware so it covers errors and static files too. Treat every inbound header as untrusted input, strip control characters from anything user-supplied before it becomes part of a response, and remember that cookies and oversized tokens are what push you into server header limits.
Testing header behaviour
Section titled “Testing header behaviour”Two assertions worth having in a suite. That the security headers appear on every response including a 404 and a 500 — a middleware that runs inside the error handler will silently stop applying them. And that a header carrying user input has control characters stripped, since header injection is invisible until someone tries it.
Both are one-line assertions and both catch regressions that no functional test would notice.