There are more HTTP headers than anyone memorizes, and half the time the question is not what a header does but which direction it travels, whether JavaScript may touch it, and what replaced it. That is what this page indexes: every header you will realistically meet, with a direction badge, a real example value, and a marker where the Fetch spec or history has retired it.

How this reference is organized

The headers are grouped by job: content and negotiation, authentication, CORS, security, caching, cookies, the proxy layer, request and response context, fetch metadata, client hints, and finally the deprecated and X-prefixed crowd. Each row carries a REQ, RES or REQ·RES badge for the direction, an example value in the exact shape you would see it on the wire, and where relevant a FORBIDDEN badge (fetch will not let a page set it) or a "superseded by" note pointing at the modern replacement.

Two groups are deliberately shallow. Caching and cookies each have enough behavior for a page of their own, so their rows here answer "what is this header" and the linked Cache-Control analyzer and Set-Cookie parser answer everything past that, from computed freshness to why a browser silently rejected a cookie. Every row is an anchor, so #etag or #set-cookie links straight to it, and the button on each row copies that link.

Inspecting headers

In the browser: Network tab, click a request, Headers pane. Chrome, Firefox and Safari all show response headers above request headers, lowercased because HTTP/2 sends them that way. "Copy as cURL" on the right-click menu is the underrated feature here, since it reproduces the request, every header included, in a form you can replay and mutate outside the browser.

On the command line, curl -i prints response headers above the body and curl -v shows both directions, request lines marked > and response lines <. Setting headers is -H "Name: value", unsetting a curl default is -H "Name:". For response headers only, curl -sD - -o /dev/null dumps them without a HEAD request, which matters because servers sometimes answer HEAD differently than GET. Server-side, most frameworks log request headers on demand, and echoing them back from a debug endpoint is the honest way to learn what a proxy chain in front of your app really adds and strips.

Rules the protocol enforces

Names are case-insensitive per RFC 9110, and lowercase on the wire in HTTP/2 and HTTP/3, so nothing should ever compare header names case-sensitively. Values defined as comma-separated lists may be split across repeated lines, and intermediaries may merge them back; Set-Cookie is the one header exempt from merging, because its dates contain commas. Size limits are implementation-defined, with 8 to 16 kB the usual band, and a 431 response almost always traces back to cookie growth rather than to headers you set deliberately.

The hop-by-hop set (Connection, Keep-Alive, TE, Trailer, Transfer-Encoding, Upgrade and the proxy auth pair) lives per connection and dies at every proxy, which explains two classic failures: WebSocket handshakes breaking behind a proxy nobody configured to forward Upgrade, and chunked encoding never surviving to the origin. And the X- convention for custom headers is officially over: RFC 6648 deprecated the prefix in 2012, after X-Forwarded-For proved that experimental names become permanent the moment two systems depend on them. New headers get plain names; the old X- ones are grandfathered, which is why the last group of this reference exists at all.

Headers JavaScript cannot touch

The Fetch spec forbids page scripts from setting the headers the browser needs to be truthful: Host, Origin, Cookie, Referer, Content-Length, Connection, Date, Via, the Access-Control-Request-* pair, and every name starting with Sec- or Proxy-. The attempt does not throw, the header just never leaves, which has burned everyone who tried to set a spoofed Origin in the console and concluded the server ignored it. The same mechanism protects reading: Set-Cookie is never visible on a fetched response, and cross-origin responses expose only a small safelist unless the server opts more headers in.

This is a browser boundary, not an HTTP one. curl, server-side code and native apps set anything they like, including Host and Origin, which is exactly why none of these headers can serve as an authentication signal on their own, and why the Sec-Fetch-* family is only meaningful when you also know the request came from a browser. The FORBIDDEN badges in the reference above are worth a scan once, because knowing which knobs do not exist saves the hour of debugging spent turning them.

Header questions

What is the difference between request headers and response headers?

Request headers travel from client to server and describe the client and what it wants: Accept, Authorization, Cookie, User-Agent. Response headers travel back and describe the server and the returned representation: Content-Type, Cache-Control, Set-Cookie, Location. A fair number are legal in both directions (Content-Type appears on a POST body as well as on the answer), and a few pairs mirror each other, like Accept-Language against Content-Language: the request states the wish, the response states the fact. In devtools both lists appear on the same request entry, which is where the confusion usually starts.

Are HTTP header names case-sensitive?

No. RFC 9110 defines field names as case-insensitive, so Content-Type, content-type and CONTENT-TYPE are the same header. HTTP/2 and HTTP/3 go a step further and require lowercase on the wire, which is why modern devtools show everything lowercased. The values, on the other hand, are often case-sensitive: an ETag comparison is byte-exact, and a base64 credential in Authorization obviously is too. Code should therefore compare names case-insensitively (or use APIs like the fetch Headers object that do it for you) and treat values by their own rules.

What is the maximum size of HTTP headers?

The spec sets no limit; every implementation does. Common defaults: nginx allows 8 kB per header line (large_client_header_buffers), Apache 8190 bytes per line (LimitRequestFieldSize) and 100 header fields, Node.js 16 kB for the whole header block (--max-http-header-size), Cloudflare 32 kB total with 16 kB per individual header. Exceeding them yields 431 (Request Header Fields Too Large) or a blunt 400, and the usual culprit is cookies accumulated over months, since every request carries all of them. If you control the server, raising the limit is a stopgap; shrinking the cookies is the fix.

Why can I not set certain headers in fetch or XMLHttpRequest?

The Fetch spec keeps a list of forbidden header names that scripts may not set, because the browser owns them for correctness and security: Host, Origin, Cookie, Referer, Content-Length, Connection, Date, Via and anything starting with Sec- or Proxy-, among others. Attempts to set them are silently ignored rather than throwing, which makes the symptom confusing: the code runs and the header simply is not there. If a page could set Origin or Sec-Fetch-Site, those headers would be worthless as security signals, which is the whole point of the list. Server-side clients like curl have no such restriction.

What are hop-by-hop headers in HTTP?

Headers that belong to one TCP connection rather than to the message, and that a proxy must strip before forwarding: Connection, Keep-Alive, Proxy-Authenticate, Proxy-Authorization, TE, Trailer, Transfer-Encoding and Upgrade. The Connection header can additionally name other headers to be treated as hop-by-hop. Everything else is end-to-end and should arrive at the origin untouched. This distinction is why Transfer-Encoding: chunked never survives a proxy (each hop re-frames the body) and why WebSocket upgrades fail through proxies that were never told to forward the Upgrade header.

Should custom HTTP headers start with X-?

No. RFC 6648 deprecated the X- prefix in 2012, because "experimental" headers never stay experimental: X-Forwarded-For became critical infrastructure and could no longer be renamed. The current advice is to pick a plain descriptive name you would be comfortable standardizing, like Request-Id instead of X-Request-Id. In practice you will keep meeting X- headers, since the RFC explicitly grandfathered the existing ones, and consistency with an existing codebase can beat purity. For anything new, skip the prefix, and remember that a custom request header makes a cross-origin request non-simple, so the server must list it in Access-Control-Allow-Headers.

How do I send a custom header with curl?

With -H: curl -H "Authorization: Bearer TOKEN" -H "Request-Id: 42" https://api.example.com. To remove a header curl sets by default, pass an empty value with a semicolon (curl -H "User-Agent;") or a colon and nothing (curl -H "Accept:"). curl -v prints the exact headers sent and received, marked with > and <, which settles any argument about what actually went over the wire. One detail worth knowing: curl -H "Expect:" suppresses the 100-continue handshake curl adds on large uploads, an old fix for proxies that mishandle it.

What is the Forwarded header, and does it replace X-Forwarded-For?

Forwarded (RFC 7239, 2014) is the standardized version of the X-Forwarded-* trio: one structured header carrying for= (client IP), proto= (original scheme), by= and host=, for example Forwarded: for=203.0.113.7;proto=https. It was meant to replace X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host, but a decade later the X- versions still dominate because every load balancer, CDN and framework already speaks them. Both share the same trust problem: any client can send a fake initial value, so an app must only trust the entries appended by its own proxies and ignore the rest.

How do I view request and response headers in Chrome DevTools?

Network tab, click the request, Headers pane: response headers on top, request headers below, both lowercased as HTTP/2 sends them. The "Raw" checkbox shows the unparsed block. Two useful tricks: right-click the request and "Copy as cURL" reproduces it with every header on the command line, and the filter box above the request list accepts has-response-header:<name>, which finds all responses carrying a given header. Provisional headers (the greyed-out warning) appear when Chrome shows what it intends to send before the request actually went out, for instance on cached or blocked requests.

Can an HTTP header appear multiple times in one message?

Yes, if its value is defined as a comma-separated list: repeating the field is then equivalent to joining the values with commas, so Accept: text/html plus Accept: application/json means the same as one combined line, and proxies are allowed to merge them. The infamous exception is Set-Cookie, whose values contain unquoted commas in the Expires date, so it must be sent as separate lines and never merged; HTTP APIs treat it specially for that reason. For anything not defined as a list, a duplicate is undefined behavior, and inconsistent duplicate handling between a proxy and a backend is a known request-smuggling ingredient.