skip to content

In PHP, how do HTTP request headers appear in $_SERVER, and why is Content-Type read from CONTENT_TYPE instead?

level: middleimportance: should knowfreq 46%

answer

  1. HTTP_ plus the uppercased name
  2. hyphens become underscores
  3. CONTENT_TYPE, CONTENT_LENGTH: CGI names
  4. X-Api-Key and X_Api_Key collide
  5. getallheaders(): a name-keyed array

basics

~10 s

Each 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 s

Under 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
<?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-insensitively

go deeper

for a junior

Recall that a header such as Accept-Language is read as $_SERVER['HTTP_ACCEPT_LANGUAGE'].

for a middle

Explain the renaming rule, why CONTENT_TYPE and CONTENT_LENGTH have no prefix, and how getallheaders() differs from $_SERVER.

for a senior

Recognise the lossy mapping and its spoofing risk, and read authentication and proxy headers only when your own infrastructure controls them.

for a principal

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