skip to content

In PHP, what does header()'s $replace argument do, and how do header_remove() and http_response_code() change a pending response?

level: middleimportance: should knowfreq 40%

answer

  1. same name replaced by default
  2. false adds a second header line
  3. header_remove() with no name clears all
  4. http_response_code() returns the old code
  5. 8.5: no effect after header('HTTP/...')

basics

~20 s

header() replaces an earlier header with the same name unless $replace is false, which adds another line. header_remove() deletes one header by name, or all with no argument. http_response_code() sets the status and returns the previous one.

solid answer

~50 s

`header(string $header, bool $replace = true, int $response_code = 0)` keeps a list of pending headers. With `$replace` true (the default), a new `Cache-Control:` replaces any earlier one, matched case-insensitively by name; with `false` it is added alongside, which you need for repeatable headers such as several `Link` lines. `header_remove('X-Powered-By')` deletes headers by name (passing a colon triggers a warning), and `header_remove()` with no argument clears every header set so far; `X-Powered-By` is there because `expose_php` defaults to on. `http_response_code(404)` sets the status and returns the previous code (200 by default in a web request); with no argument it returns the current code, or `false` outside a web server. Prefer it to `header('HTTP/1.1 404 Not Found')`: in PHP 8.5, calling `http_response_code()` after such a status line only warns and has no effect. All of these fail once headers are sent.

code

php · 12 lines
php
<?php
declare(strict_types=1);

header('Cache-Control: no-store');
header('Cache-Control: private, max-age=0');          // replaces the first
header('Link: </css/admin.css>; rel=preload; as=style', false);
header('Link: </js/admin.js>; rel=preload; as=script', false); // both Link lines kept
header_remove('X-Powered-By');                          // name only, no colon

$previous = http_response_code(404);                    // int(200) in a web request
var_dump(http_response_code());                         // int(404)
print_r(headers_list());

go deeper

for a junior

Recall that header() replaces a same-named header by default, false adds another, and http_response_code() sets the status.

for a middle

Explain header_remove() with and without a name, what http_response_code() returns in each mode, and the HTTP/ status-line special case.

for a senior

Use these to override framework defaults safely, remove X-Powered-By, and avoid the 8.5 status-line conflict by setting codes one way only.

for a principal

Decide which layer owns response headers - server config, framework middleware or handlers - so overrides are deliberate and auditable.

## The pending header list Until the first byte of body output, PHP holds the response status and a list of headers. The functions in this question edit that list; nothing reaches the client until output starts or the script ends. | Function | Signature (PHP 8.5) | Effect | |---|---|---| | `header()` | `header(string $header, bool $replace = true, int $response_code = 0): void` | add or replace a header, optionally set the status | | `header_remove()` | `header_remove(?string $name = null): void` | delete headers by name, or all of them | | `http_response_code()` | `http_response_code(int $response_code = 0): int\|bool` | set or read the status code | | `headers_list()` | `headers_list(): array` | read the pending header lines | ## `header()` and `$replace` With the default `$replace = true`, a header replaces every earlier header whose **name** matches, case-insensitively. That is what you want for single-valued headers: - a framework set `Cache-Control: no-store`, and a CSV export route wants `Cache-Control: private, max-age=0` - the second call replaces the first; - `Content-Type` set twice keeps only the last. With `$replace = false`, the new line is **added** and earlier ones stay. Use it for headers that may legitimately repeat, such as several `Link:` lines for preloads, or several challenges in `WWW-Authenticate`. The third argument sets the status code in the same call, which is convenient for redirects. A few special cases are built into `header()`: 1. A line starting with `HTTP/` (for example `HTTP/1.1 404 Not Found`) sets the **status line** rather than adding a header. 2. `Location:` sets a redirect code if none of the redirect kind is present. 3. `Content-Type:` with a `text/...` type and no charset gets `;charset=UTF-8` appended, from `default_charset`. 4. A header containing CR, LF or NUL is refused with a warning, which blocks header injection. ## `header_remove()` `header_remove('X-Powered-By')` removes every pending header with that name. The name must not include a colon; `header_remove('X-Powered-By:')` fails with "Header to delete may not contain colon." Called with no argument, it removes **all** headers set so far. `X-Powered-By` exists in the first place because `expose_php` defaults to on (in the compiled defaults and in `php.ini-production`); turning that off is the cleaner fix, but removing the header per response works where you cannot change the configuration. ## `http_response_code()` - **Setting:** `http_response_code(404)` stores the new code and returns the **previous** one - `200` by default in a web request - or `true` where no code existed yet, such as the CLI. - **Reading:** with no argument it returns the current code (200 by default in a web request), or `false` when there is no web server context, such as the CLI. - **After output:** it warns "Cannot set response code - headers already sent" and returns `false`. Setting the status with `header('HTTP/1.1 404 Not Found')` stores a whole status line, and that line wins over the numeric code. In PHP 8.5 the engine makes this explicit: a later `http_response_code()` call emits "Calling http_response_code() after header('HTTP/...') has no effect" and leaves the status line in place. Use `http_response_code()` consistently and let PHP build the status line; it also avoids hard-coding the protocol version. ## Worked example: an admin export route An admin panel's framework sets defaults on every response: `Cache-Control: no-store`, a `Content-Security-Policy`, and `X-Powered-By` from `expose_php`. A CSV export route needs to adjust them before sending the file: - `header('Cache-Control: private, max-age=0');` replaces the framework's value, because `$replace` defaults to `true`; - `header_remove('X-Powered-By');` drops the version banner for this response; - `header('Content-Type: text/csv');` sends `text/csv;charset=UTF-8`, because PHP appends the default charset to `text/` types; - `http_response_code(200)` is unnecessary unless an earlier layer set something else; if it did, the call returns that earlier code, which is handy for logging. Each of these must happen before the first row of the file is written. ## Inspecting what will be sent `headers_list()` returns the pending header lines as strings; tests of legacy code sometimes assert on it. It does not include the status line, which `http_response_code()` reports.

  • In PHP 8.5, what happens if a script calls header('HTTP/1.1 404 Not Found') and later http_response_code(500)?
    The status line from `header()` wins. Since PHP 8.5.0 the `http_response_code(500)` call emits the warning "Calling http_response_code() after header('HTTP/...') has no effect", and the client still receives 404. Setting statuses only through `http_response_code()` avoids the conflict.
  • How do you send two Link headers with header() without the second replacing the first?
    Pass `false` as the second argument on the later calls, for example `header('Link: <...>; rel=preload', false)`. With the default `true`, each call replaces every earlier header of the same name, so only the last would be sent. For cookies specifically, `setcookie()` already adds rather than replaces.

saying these in an interview costs you the question

  • header() always appends, so repeated calls send duplicate headers.
  • header_remove('X-Powered-By:') is the right way to remove it.
  • http_response_code(404) returns the new status code.
  • header('HTTP/1.1 404') and http_response_code() can be mixed freely.
  • header_remove() still works after output has started.