In PHP, why must header('Location: ...') be followed by exit, and which status code does the redirect send?
answer
- header() only queues a header
- code after it still runs
- 302 unless a 3xx or 201 is set
- third argument sets the status
- 303 after a POST save
basics
~20 sheader('Location: ...') only queues a header; the script keeps running, so code after it still executes and its output is sent. Call exit right after. PHP adds a 302 unless a code is given, so pass 303 or 301 explicitly.
solid answer
~40 s`header()` adds a line to the pending headers and returns; it does not end the request. Without `exit`, everything after it still runs: a later `echo` still produces a body, a guard that redirects unauthorised users still renders the protected page for any client that ignores the redirect, and later code may even replace the `Location`. So write `header('Location: /admin/users', true, 303); exit;`. For the status, PHP adds a redirect code only if the current code is not already `201` or a `3xx`: by default `302`, and in the pinned source `303` for a non-GET request when the server API reports HTTP/1.1 or later (Apache's module does; PHP-FPM reports 1.0 internally and gets 302). Don't depend on that: pass the code as `header()`'s third argument, or call `http_response_code()` first.
code
php · 15 lines<?php
declare(strict_types=1);
if (!isAdmin()) { // application function
header('Location: /login', true, 302);
exit; // without it, the page below still renders
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
saveUser($_POST); // application function
header('Location: /admin/users', true, 303);
exit;
}
require __DIR__ . '/templates/user-form.php';go deeper
Recall that header() does not stop the script, so a redirect is always followed by exit, and that the status can be passed as the third argument.
Explain PHP's default code choice for Location, why it differs between server APIs, and when to choose 303, 302, 307, 301 or 308.
Show how a missing exit turns an access guard into a data leak and how later code can override the redirect; review handlers for it.
Standardise redirects in one helper or response object that always sets an explicit code and ends the request, so no handler can forget either.
## `header()` queues, it does not stop `header(string $header, bool $replace = true, int $response_code = 0): void` stores a header in the pending list. It returns immediately, and the script continues until it reaches the end or an `exit`. Nothing about a `Location` header ends the request. That has three practical consequences: - **Code after the redirect runs.** Database writes, logging, or a second `header('Location: ...')` that overrides the first all happen. - **Output after the redirect is sent.** The response carries the `Location` header *and* a body. Browsers follow the redirect and ignore the body, but a client that does not follow redirects sees everything. - **Access guards leak.** A check like "if not logged in, redirect to login" without `exit` goes on to render the admin page into the body of the redirect response. The fix is a habit: every redirect is immediately followed by `exit` (or a `return` from the front controller that ends the request). ## Which status code PHP sends When you set a `Location` header, PHP looks at the current response code: | Situation | Status sent | |---|---| | a `3xx` or `201` already set | unchanged | | third argument of `header()` given | that code | | otherwise, a GET or HEAD request | `302` | | otherwise, a non-GET request and the server API reports HTTP/1.1 or later | `303` | | otherwise | `302` | The manual's `header()` page mentions only the 302 case. The pinned source adds the 303 rule, and which branch applies depends on the server API: the Apache module passes the real protocol version, while PHP-FPM and CGI report HTTP/1.0 internally, so a redirect after a POST under FPM gets `302`. Browsers treat a 302 after a POST as "fetch the new URL with GET" in practice, but the result should not depend on the deployment. ## Choosing the code explicitly Pass the code yourself: - `303` - after a successful POST (Post/Redirect/Get): "see the result at this URL, with GET"; - `302` or `307` - a temporary redirect; `307` keeps the original method and body; - `301` or `308` - a permanent move; browsers and caches remember it, so use it only when you mean it. Two equivalent ways to set it: 1. `header('Location: /admin/users', true, 303);` - status and header in one call. 2. `http_response_code(303); header('Location: /admin/users');` - PHP keeps an existing 3xx. ## Reviewing code for missing exits Missing `exit` calls are easy to spot in review once you look for them: - search for every `header('Location` and check the next statement ends the request; - look at helper functions that redirect: if `redirect($url)` only calls `header()`, every caller must still `exit`, so make the helper end the request itself (and give it the return type `never`); - check guards at the top of scripts (authentication, permissions, CSRF failures), where a missing `exit` does the most damage; - check that nothing between the redirect and `exit` produces output or changes state. A static analyser can help here: a function declared as returning `never` tells both readers and tools that control does not come back. ## A complete admin-panel redirect After an admin saves a user record, the handler should: 1. perform the save; 2. make sure no output has been produced yet (otherwise the header is dropped); 3. set `Location` with an explicit `303`; 4. `exit`. One related detail: a `Location` header containing a newline is rejected with the warning "Header may not contain more than a single header, new line detected", which stops header injection through a crafted URL.
- What exactly leaks if an access check redirects to the login page without exit?The script keeps running past the check and renders the protected page into the response body. A browser follows the `Location` header and never shows it, but any client that does not follow redirects, or a tool inspecting the raw response, receives the full page. Any state-changing code after the check also still runs.
- Under PHP-FPM, which status does header('Location: /done') send after a POST if no code is given?`302`. The pinned source sends `303` for a non-GET request only when the server API reports HTTP/1.1 or later, and PHP-FPM reports HTTP/1.0 internally, so the default branch applies. Passing `303` as the third argument makes the result the same on every server API.
saying these in an interview costs you the question
- header('Location: ...') ends the script like a return.
- PHP always sends 301 for a Location header.
- A body after a redirect is never sent to the client.
- The default redirect status is the same under every server API.
- A Location header ignores a 3xx code set earlier.