PHP Redirect: How to Redirect URLs in PHP

How to redirect URLs in PHP using the header() function. Covers 301 and 302 redirects, common mistakes like headers already sent, and framework-specific methods in Laravel and Symfony.

PHP handles redirects through HTTP headers. The core mechanism is the header() function, which sends raw HTTP headers to the browser before any page content. While the concept is straightforward, PHP's output buffering rules and the variety of frameworks in use create several ways to get it wrong. This guide covers the correct approach for plain PHP and for the most popular frameworks. For background on how HTTP redirects work, see HTTP Redirect Status Codes.

The Basic PHP Redirect

The standard way to redirect in PHP is to send a Location header followed by an exit or die statement:

<?php
header('Location: https://example.com/new-page');
exit;

This sends an HTTP 302 Found response by default. The browser receives the response, reads the Location header, and makes a new request to the specified URL. The exit statement is critical. Without it, PHP continues executing the rest of the script, which wastes server resources and can cause unexpected behavior if subsequent code modifies headers or sends output.

Setting the Status Code

To send a 301 Moved Permanently redirect instead of a 302, you have two options:

<?php
// Option 1: Set the response code explicitly
http_response_code(301);
header('Location: https://example.com/new-page');
exit;

// Option 2: Include the status in the header call
header('Location: https://example.com/new-page', true, 301);
exit;

Both approaches are equivalent. The third parameter of header() accepts the HTTP status code directly. The second parameter (true) tells PHP to replace any existing Location header rather than adding a duplicate.

For the differences between 301 and 302 and when to use each, see 301 vs 302 Redirects.

Other Status Codes

PHP supports all standard redirect status codes:

<?php
// 303 See Other -- redirect after POST
header('Location: /thank-you', true, 303);
exit;

// 307 Temporary Redirect -- preserves HTTP method
header('Location: /temp-page', true, 307);
exit;

// 308 Permanent Redirect -- preserves HTTP method
header('Location: /new-page', true, 308);
exit;

The 303 status code is particularly useful after processing form submissions. It tells the browser to follow the redirect with a GET request, preventing the "resubmit form" dialog when the user presses the back button. For a full reference, see Types of Redirects.

The "Headers Already Sent" Error

The most common PHP redirect mistake produces this error:

Warning: Cannot modify header information - headers already sent by (output started at /path/to/file.php:12)

PHP sends HTTP headers to the browser as soon as any output is generated. Output includes HTML, whitespace before the opening <?php tag, echo statements, print statements, and even a byte order mark (BOM) in your file encoding. Once output starts, you cannot send new headers, and the redirect fails.

How to Fix It

Remove whitespace before <?php. Check that there are no blank lines, spaces, or invisible characters before the opening PHP tag. This includes files that are included or required before the redirect.

<?php
// Correct: no whitespace before the opening tag
header('Location: /new-page');
exit;

Do not output anything before the redirect. If your script echoes HTML or sends any output before the redirect call, the redirect will fail. Restructure your code so the redirect decision happens before any output.

Use output buffering. If you cannot restructure the code, output buffering captures all output in memory instead of sending it immediately:

<?php
ob_start();

// ... code that might produce output ...

if ($shouldRedirect) {
    ob_end_clean(); // discard buffered output
    header('Location: /new-page', true, 301);
    exit;
}

ob_end_flush(); // send buffered output

ob_start() at the beginning of your script prevents the "headers already sent" error by holding all output in a buffer. You can then discard the buffer with ob_end_clean() before sending the redirect. This works but is a workaround, not a solution. The real fix is to separate your logic from your output.

Check for BOM characters

If you are certain there is no whitespace before your PHP tag but still get the headers-already-sent error, your file may contain a Unicode byte order mark (BOM). Open the file in a hex editor or use an editor that shows invisible characters. Save the file as UTF-8 without BOM.

Redirecting with Query Parameters

To include query parameters in the redirect URL, build the URL string before passing it to header():

<?php
$productId = 42;
$category = 'electronics';
$url = sprintf('/products?id=%d&category=%s',
    $productId,
    urlencode($category)
);
header('Location: ' . $url, true, 301);
exit;

Always use urlencode() for values that might contain special characters. Failing to encode query parameters can break the URL or create security vulnerabilities.

Security: Preventing Open Redirects

Never redirect to a user-supplied URL without validation:

<?php
// DANGEROUS: open redirect vulnerability
$url = $_GET['redirect_to'];
header('Location: ' . $url);
exit;

An attacker can craft a link like yoursite.com/login?redirect_to=https://evil.com and use it in phishing attacks. The victim sees your domain in the link but ends up on the attacker's site.

Validate the redirect target against a whitelist or ensure it is a relative URL on your own domain:

<?php
$url = $_GET['redirect_to'] ?? '/';

// Only allow relative URLs
if (strpos($url, '/') !== 0 || strpos($url, '//') === 0) {
    $url = '/';
}

header('Location: ' . $url);
exit;

For more on this vulnerability, see What Is an Open Redirect?.

Laravel Redirects

Laravel provides a fluent redirect API through the redirect() helper and the Redirect facade:

// Basic redirect
return redirect('/dashboard');

// Redirect with status code
return redirect('/new-page', 301);

// Redirect to a named route
return redirect()->route('profile', ['id' => 1]);

// Redirect back to the previous page
return redirect()->back();

// Redirect with flash data
return redirect('/dashboard')->with('status', 'Profile updated');

// Redirect to an external URL
return redirect()->away('https://external-site.com');

Laravel's redirect() returns a RedirectResponse object. The default status code is 302. For permanent redirects, pass 301 as the second argument or use the Route::permanentRedirect() method in your route definitions:

// In routes/web.php
Route::permanentRedirect('/old-path', '/new-path');
Route::redirect('/temp-path', '/other-path', 302);

Route::permanentRedirect() sends a 301 response. Route::redirect() sends a 302 by default but accepts a status code as the third parameter.

Redirect After Validation Failure

Laravel automatically redirects back to the previous page when form validation fails. The old input and error messages are flashed to the session:

public function store(Request $request)
{
    $validated = $request->validate([
        'email' => 'required|email',
        'name' => 'required|string|max:255',
    ]);

    // If validation fails, Laravel redirects back automatically
    // If validation passes, continue processing
    User::create($validated);

    return redirect()->route('users.index')
        ->with('success', 'User created');
}

Symfony Redirects

Symfony controllers use the redirectToRoute() and redirect() methods:

use Symfony\Component\HttpFoundation\Response;

class PageController extends AbstractController
{
    // Redirect to a route
    public function oldPage(): Response
    {
        return $this->redirectToRoute('new_page', [], 301);
    }

    // Redirect to a URL
    public function externalRedirect(): Response
    {
        return $this->redirect('https://example.com', 301);
    }
}

Both methods return a RedirectResponse object. The default status code is 302. For permanent redirects in your routing configuration:

# config/routes.yaml
old_page:
    path: /old-page
    controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController
    defaults:
        route: new_page
        permanent: true

WordPress Redirects

WordPress provides the wp_redirect() function:

<?php
wp_redirect('/new-page', 301);
exit;

The wp_safe_redirect() function adds security by only allowing redirects to whitelisted hosts:

<?php
wp_safe_redirect('/dashboard', 302);
exit;

For managing redirects without code, see the WordPress Redirect Guide.

Relative vs Absolute URLs

The HTTP specification (RFC 9110) allows both relative and absolute URLs in the Location header:

// Absolute URL
header('Location: https://example.com/new-page');

// Relative URL
header('Location: /new-page');

Modern browsers handle relative URLs correctly, resolving them against the current host. However, some older HTTP clients and crawlers may not. For maximum compatibility, especially for cross-domain redirects, use absolute URLs.

Redirect Chains and Performance

Each redirect adds a round trip between the browser and the server. If your PHP application redirects to URL A, which redirects to URL B, which redirects to URL C, the user waits for three HTTP round trips before seeing any content. Keep redirect chains to a single hop whenever possible.

A common source of chains in PHP applications is combining an HTTP-to-HTTPS redirect with a www-to-non-www redirect with an application-level redirect. Handle as many of these as possible at the web server level (Apache or Nginx) rather than in PHP, and point directly to the final URL. For more on this topic, see Redirect Chains Explained.

Testing Your Redirects

After implementing redirects, verify they work correctly using curl:

curl -I -L https://example.com/old-page

The -I flag shows only headers, and -L follows redirects. Check that the status code matches your intent (301 vs 302) and that the final destination is correct. For a more visual approach, use Redirect Tracer to see every hop in the chain.

References

  1. PHP Manual, "header()," https://www.php.net/manual/en/function.header.php
  2. PHP Manual, "http_response_code()," https://www.php.net/manual/en/function.http-response-code.php
  3. Laravel Documentation, "HTTP Redirects," https://laravel.com/docs/responses#redirects
  4. Symfony Documentation, "Controller," https://symfony.com/doc/current/controller.html
  5. IETF, "RFC 9110 - HTTP Semantics, Section 15.4: Redirection 3xx," June 2022. https://httpwg.org/specs/rfc9110.html#status.3xx

Verify your PHP redirects are working correctly

Trace every redirect hop from your PHP application, check status codes, and catch redirect chains before they become a problem.

Try Redirect Tracer