In PHP, how do HTTP request headers appear in $_SERVER, and why is Content-Type read from CONTENT_TYPE instead?
answer
- HTTP_ plus the uppercased name
- hyphens become underscores
- CONTENT_TYPE, CONTENT_LENGTH: CGI names
- X-Api-Key and X_Api_Key collide
- getallheaders(): a name-keyed array
basics
~10 sEach request header becomes $SERVER['HTTP' . NAME], uppercased with hyphens as underscores, so Accept-Language is HTTP_ACCEPT_LANGUAGE. Content-Type and Content-Length follow the CGI convention instead, arriving as CONTENT_TYPE and CONTENT_LENGTH without the prefix.
solid answer
~50 sUnder a web server, PHP receives the request's metadata as CGI-style variables and copies them into `$_SERVER`. A header becomes `HTTP_` plus its name in upper case with hyphens replaced by underscores: `Accept-Language` is `$_SERVER['HTTP_ACCEPT_LANGUAGE']`, `X-Request-Id` is `HTTP_X_REQUEST_ID`. Two headers are special under the CGI convention: the body's type and length arrive as `CONTENT_TYPE` and `CONTENT_LENGTH`, with no prefix. Whether an `HTTP_CONTENT_TYPE` copy also exists depends on the server — PHP's built-in development server sets both — so read the unprefixed keys. The mapping is lossy: `X-Api-Key` and `X_Api_Key` both produce `HTTP_X_API_KEY`, and every `HTTP_*` value is client-controlled. When you want an array keyed by header name, `getallheaders()` returns one: under Apache's module it keeps the names as received, while under PHP-FPM it rebuilds them from the `HTTP_*` variables (`X-Request-Id`), so the collision survives there.
code
php · 12 lines<?php
// Request headers:
// Content-Type: application/json
// Accept-Language: pt-PT
// X-Request-Id: 7f3a
$type = $_SERVER['CONTENT_TYPE'] ?? ''; // "application/json" - no HTTP_ prefix
$language = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''; // "pt-PT"
$requestId = $_SERVER['HTTP_X_REQUEST_ID'] ?? null; // "7f3a"
$headers = array_change_key_case(getallheaders(), CASE_LOWER);
$same = $headers['x-request-id'] ?? null; // "7f3a", looked up case-insensitivelygo deeper
Recall that a header such as Accept-Language is read as $_SERVER['HTTP_ACCEPT_LANGUAGE'].
Explain the renaming rule, why CONTENT_TYPE and CONTENT_LENGTH have no prefix, and how getallheaders() differs from $_SERVER.
Recognise the lossy mapping and its spoofing risk, and read authentication and proxy headers only when your own infrastructure controls them.
Decide which headers the edge strips, sets and forwards to PHP, so application code can rely on a documented, trusted subset.
## From header to $_SERVER key When PHP runs behind a web server, the server passes each request to PHP together with a set of **CGI-style variables** — the convention defined by the CGI/1.1 specification. PHP copies them into the `$_SERVER` array. Request headers are among them, renamed by a fixed rule: 1. Convert the header name to upper case. 2. Replace every hyphen with an underscore. 3. Prefix the result with `HTTP_`. | Header sent | `$_SERVER` key | |---|---| | `Accept-Language: pt-PT` | `HTTP_ACCEPT_LANGUAGE` | | `User-Agent: ...` | `HTTP_USER_AGENT` | | `X-Request-Id: 7f3a` | `HTTP_X_REQUEST_ID` | | `Host: booking.example` | `HTTP_HOST` | | `Content-Type: application/json` | `CONTENT_TYPE` | | `Content-Length: 348` | `CONTENT_LENGTH` | The PHP manual notes that there is no guarantee every server provides every entry; most are defined by the CGI specification and are likely to be present. ## Why Content-Type and Content-Length are different The CGI convention treats the body's type and length as properties of the request itself, not as ordinary headers, and names them `CONTENT_TYPE` and `CONTENT_LENGTH`. PHP-FPM reads exactly those two variables from the web server to learn about the body. Whether an extra `HTTP_CONTENT_TYPE` also appears depends on the server in front: PHP's own built-in development server registers **both** spellings. Code that looks only for `HTTP_CONTENT_TYPE` therefore works on a developer's laptop and fails in production — read `CONTENT_TYPE`. ## The Authorization header For HTTP Basic authentication PHP fills `$_SERVER['PHP_AUTH_USER']` and `$_SERVER['PHP_AUTH_PW']` from the `Authorization` header when it receives it; PHP-FPM reads it from `HTTP_AUTHORIZATION`. Whether the header reaches PHP at all depends on how the web server is configured to pass it, so a missing `Authorization` value is often a server-configuration issue rather than a client one. ## A lossy mapping Because the rule upper-cases names and turns hyphens into underscores, **different headers can map to the same key**: - `X-Api-Key`, `x-api-key` and `X_Api_Key` all become `HTTP_X_API_KEY`; - which one wins depends on the server and the order it passes them. If a front proxy sets a trusted header such as `X-Client-Verified` and the client sends `X_Client_Verified`, the two can collide in `$_SERVER`. Many web servers can be configured to drop header names containing underscores; the safe assumption in PHP code is that an `HTTP_*` value may have come from the client under a different spelling. ## getallheaders() `getallheaders()` returns the request headers as an array keyed by header name — `['Content-Type' => 'application/json', 'X-Request-Id' => '7f3a', ...]`. It is an alias of `apache_request_headers()` and is available under Apache's module and PHP-FPM. The two differ in where the names come from: Apache's module reports the names as received, while PHP-FPM **rebuilds** them from the `HTTP_*` variables — turning underscores back into hyphens and lower-casing all but the first letter of each part — plus `Content-Type` and `Content-Length` from the CGI names. Under PHP-FPM, then, the underscore collision described above is not undone. Three cautions: - header names are case-insensitive in HTTP, so look them up case-insensitively (for example by normalising the keys with `array_change_key_case()`); - do not expect the exact spelling the client typed, since it depends on the SAPI; - the values are exactly as untrusted as the `HTTP_*` entries. ## Under the CLI When a script runs from the command line there is no HTTP request, so `$_SERVER` has no `HTTP_*` entries, no `REQUEST_METHOD` and no `CONTENT_TYPE`; instead it carries process information such as the script name and the environment. Code shared between a web entry point and a console command should therefore read headers with a default (`?? null`) rather than assuming they exist, or better, receive them from the web layer as arguments. ## Reading headers safely - Use `$_SERVER['HTTP_X_REQUEST_ID'] ?? null`; a missing header is a missing key, not an empty string. - Check the type and length of a header value before using it as an identifier. - Never grant access based on a header the client could have sent unless your own proxy strips and re-sets it.
- Why might code that reads $_SERVER['HTTP_CONTENT_TYPE'] work locally and fail in production?PHP's built-in development server registers the body type under both `CONTENT_TYPE` and `HTTP_CONTENT_TYPE`, but behind a production web server with PHP-FPM only the CGI name `CONTENT_TYPE` is dependable. Reading the unprefixed key works everywhere.
- Can two different headers end up under the same $_SERVER key?Yes. The mapping upper-cases the name and turns hyphens into underscores, so `X-Api-Key` and `X_Api_Key` both become `HTTP_X_API_KEY`. Depending on what the web server forwards, a client can shadow a header your proxy sets, so do not trust an `HTTP_*` value your own infrastructure does not strip and re-set.
saying these in an interview costs you the question
- Headers keep their original spelling as $_SERVER keys
- Content-Type is always available as HTTP_CONTENT_TYPE
- Values under HTTP_* are set by the server and can be trusted
- Header names map one-to-one to $_SERVER keys, with no collisions
- Under PHP-FPM, getallheaders() returns names exactly as the client typed them