PHP · TUTORIAL

PHP Redirects: How to Use header() Correctly

Send visitors to a new page without output errors, redirect loops or an untrusted destination.

A PHP redirect tells the browser to request a different URL. It is useful after a successful form submission, when a page has moved, or when application logic chooses a destination. The mechanism is an HTTP response header, not a piece of visible HTML. That distinction explains the most common problem: by the time the code tries to redirect, the page has already begun sending content.

On a site built from PHP layout includes, redirect decisions belong before the header template renders. If you include a file that prints a document declaration or navigation first, you may have already committed the response body. Plan the request’s outcome before building the page, then use the layout only when the request will actually display content.

A basic redirect has two steps

Send a Location header and stop executing the script. Set a status code explicitly so a reader does not have to infer the redirect type from PHP’s default behavior. The destination below is a fixed path on the same site, and the 302 indicates a temporary redirect.

<?php
header('Location: /programming/', true, 302);
exit;

The second argument controls replacement of a previous header of the same kind. More importantly, exit prevents later PHP statements from running. Sending a Location header does not itself stop execution. Without the exit, the program might still update records, send an email, or render a page that it should never reach on this request.

Choose the status for the behavior

Use a permanent redirect such as 301 when an old content URL has been replaced permanently. A temporary 302 is useful while testing or when a destination can change. A 303 is particularly useful after processing a POST form: it instructs the client to retrieve the next page with a GET request. A 307 or 308 preserves the original request method, which is a different behavior from a form confirmation page.

For example, a contact form can process a valid submission, place a success message in the session, and redirect with 303 to the Contact page. The browser then displays the page through GET. Refreshing that confirmation page does not repeat the original POST. This pattern is often called Post/Redirect/Get. It reduces accidental repeat submissions, though it does not replace server-side duplicate protection.

<?php
// Only after validating and processing the form successfully:
$_SESSION['message'] = 'Thank you. Your message was sent.';
header('Location: /contact/', true, 303);
exit;

Solve headers already sent at the source

If PHP reports that headers have already been sent, inspect the first output location. Common causes include an echo statement, HTML outside PHP tags, a debug dump, whitespace before the opening PHP tag, or a byte order mark at the start of a file. An included layout can also produce output even if the redirecting file itself looks clean.

For PHP-only configuration and helper files, omitting the closing PHP tag helps avoid accidental trailing whitespace. Put request processing before template includes. During debugging, headers_sent() can identify the file and line associated with the first output; record that information in a server log rather than publishing internal paths to visitors.

<?php
if (headers_sent($file, $line)) {
    error_log('Output started at ' . $file . ':' . $line);
    exit;
}
header('Location: /tools/', true, 302);
exit;

Output buffering can delay when content is sent, but it should not be used as a substitute for understanding the request flow. A small website is easier to maintain when redirects happen before rendering. That also avoids making the behavior depend on a hosting setting that may differ between development and production.

Do not redirect to unchecked input

A link such as ?next=... can be convenient, but copying that value straight into a Location header can create an open redirect to an attacker’s site. An apparently harmless local path may also begin with two slashes, which a browser can treat as a reference to another host. For a small application, map a limited set of destination names to fixed paths.

<?php
$destinations = [
    'home' => '/',
    'tools' => '/tools/',
    'programming' => '/programming/',
];
$choice = $_GET['next'] ?? 'home';
if (!is_string($choice) || !isset($destinations[$choice])) {
    $choice = 'home';
}
header('Location: ' . $destinations[$choice], true, 302);
exit;

The visitor supplies only a choice name, while the server controls the actual URL. This is easier to audit than a complicated URL-validation expression. If you genuinely need external destinations, define a precise allowlist and reject malformed URLs and control characters. Avoid treating a successful HTML escape as validation for an HTTP response header; these are different output contexts.

Use Apache for broad URL moves

PHP is appropriate when a redirect depends on application logic, such as whether a form was processed. A simple permanent directory move is usually clearer in the web-server configuration. The directory redirect tutorial shows how a single Apache rule preserves the rest of each path. That avoids creating a PHP redirect file for every moved page.

Check the whole journey

Inspect the response status and Location header, then confirm the browser reaches the intended final page. Check that the destination exists and that it does not redirect back to the source. When testing a permanent change, consider starting with a temporary redirect because clients may cache permanent ones. Test a form submission separately from a normal page visit; their request methods are different.

A reliable redirect is a short piece of code with a deliberate destination and status. Decide first, send the header before output, and stop execution. Those three habits prevent most maintenance surprises in a hand-built PHP site.

Reference: PHP header() and HTTP Semantics, RFC 9110.