In PHP, what does header()'s $replace argument do, and how do header_remove() and http_response_code() change a pending response?
answer
- same name replaced by default
- false adds a second header line
- header_remove() with no name clears all
- http_response_code() returns the old code
- 8.5: no effect after header('HTTP/...')
basics
~20 sheader() 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
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
Recall that header() replaces a same-named header by default, false adds another, and http_response_code() sets the status.
Explain header_remove() with and without a name, what http_response_code() returns in each mode, and the HTTP/ status-line special case.
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.
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.