Redirects and Status Codes

A redirect is a 3xx status plus a Location header; send it with header(), then exit, and use 303 after a POST.

Syntaxheader('Location: /account', true, 303); exit;

A redirect is a response that says "not here, go there". It carries a 3xx status code and a Location header holding the new URL. The browser then makes a second request to that URL on its own.

Sending one

Two lines: header('Location: /account', true, 303); and then exit;. The third argument of header() sets the status code. Leave it out and PHP sends 302 along with the Location header, unless a 201 or another 3xx has already been set.

Why exit

header() queues a header. It does not stop the script. Without exit, every line below still runs, which is how one click ends up storing the same record twice.

Post, redirect, get

After a successful POST, redirect rather than printing the result. The browser's next request is a GET, so reloading the page repeats that harmless GET instead of offering to submit the form again. 303 is the code that means exactly this.

The codes worth knowing

CodeMeaning
301Moved permanently. Browsers cache it, so it is hard to take back.
302Found. Temporary, and the default PHP sends.
303See other. The next request will be a GET. Use after a POST.
307Temporary, and the method and body are kept.
308Permanent, and the method and body are kept.

What trips people up

Never place a raw query parameter in Location. Accept your own paths only.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Send a visitor to a thank-you page after their form is stored.
  • Stop a page refresh from submitting the same order twice.
  • Move an old URL to its replacement without breaking existing links.
  • Return someone to the page they asked for after signing in, safely.

More examples

Post, redirect, get

Printing a confirmation straight from the POST leaves the browser sitting on a POST response, so a refresh re-submits the form and the visitor gets a warning dialog about it. Redirecting to a GET removes both problems. The message is carried in the query string here, which is visible in the URL and easy to fake; a value stored for that one visitor server-side is the better carrier once sessions are available. This does not run in the sandbox, which has no request.

Example · php
<?php

declare(strict_types=1);

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // store the comment here
    header('Location: /articles/7?saved=1', true, 303);
    exit;
}

$saved = isset($_GET['saved']);

if ($saved) {
    echo '<p>Your comment was saved.</p>';
}

A permanent move to the canonical host

One page should have one address, so requests to the other spelling of the host are sent to the canonical one, keeping the path they asked for. 301 is the honest code here, but browsers remember it for a long time, so a 301 published by mistake keeps sending visitors to the wrong place after the code is fixed. Use 302 while you are unsure and change it to 301 when the move really is permanent.

Example · php
<?php

declare(strict_types=1);

$host = $_SERVER['HTTP_HOST'] ?? '';
$target = $_SERVER['REQUEST_URI'] ?? '/';

if ($host === 'example.com') {
    // 301 tells browsers and search engines the move is permanent
    header('Location: https://www.example.com' . $target, true, 301);
    exit;
}

Refusing to redirect off site

A sign-in page often takes ?next= so it can return the visitor to the page they wanted. Passing that value straight to Location turns your domain into a redirector for anyone sending out links, and the visitor sees your name in the URL they clicked. Allow your own paths only. PHP also refuses a header value containing a newline, which blocks the related trick of smuggling extra headers through the same parameter.

Example · php
<?php

declare(strict_types=1);

function safeNext(?string $next, string $fallback = '/'): string
{
    if ($next === null || $next === '') {
        return $fallback;
    }

    // A single leading slash is one of our own paths.
    // Two slashes, or a scheme, point at another site.
    if (!str_starts_with($next, '/') || str_starts_with($next, '//')) {
        return $fallback;
    }

    return $next;
}

$target = safeNext($_GET['next'] ?? null, '/account');

header('Location: ' . $target, true, 303);
exit;

Discussion

  • Be the first to comment on this lesson.