HTTP Headers and Redirects

JA JASWANT Updated 17 Oct 2026
7 min read ·Lesson 104 of 109
In this post

What Are HTTP Headers? In simple terms: Every HTTP response consists of two parts: headers (information about the response) and the body (the page, image, or JSON itself). Headers tell the browser thi

What Are HTTP Headers?

In simple terms: Every HTTP response consists of two parts: headers (information about the response) and the body (the page, image, or JSON itself). Headers tell the browser things like "this is JSON", "download this as a file", "go to another URL", "cache this for one hour", or "don't allow this page inside a frame".

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Cache-Control: no-store

<!DOCTYPE html>
...

Technically: PHP sends a status line and headers before the first byte of the body. The header() function adds or replaces response headers, and http_response_code() sets the status code. Because headers come first, they must be set before any output — or while output is still buffered.


Sending Headers

Syntax

header(string $header, bool $replace = true, int $response_code = 0): void
<?php
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
http_response_code(201);

echo json_encode(['status' => 'created']);
  • Each call adds one header line.
  • By default, a header with the same name replaces an earlier one; pass false as the second argument to add another header with the same name (for example, multiple Set-Cookie headers).
  • http_response_code() sets the status code; without an argument, it returns the current code.

Inspecting and Removing Headers

FunctionPurpose
headers_list()Headers prepared for sending
header_remove('X-Powered-By')Remove a header (here, the one revealing the PHP version)
headers_sent($file, $line)Whether headers have already been sent, and where output started

"Headers Already Sent"

<?php
echo "Hello";
header('Location: /login.php');

Output

Hello
PHP Warning:  Cannot modify header information - headers already sent by (output started at /path/page.php:2) in /path/page.php on line 3

The echo sent the response headers (and the start of the body), so the redirect is impossible. Common hidden causes are whitespace or a blank line before <?php, a closing ?> followed by a newline in an included file, or a UTF-8 byte order mark (BOM) at the start of a file. The warning tells you the exact file and line where output started.

Fix: Send all headers before output — typically by processing the request first and rendering the template last. Omitting the closing ?> in pure PHP files prevents accidental whitespace.


Common Response Headers

HeaderExamplePurpose
Content-Typetext/html; charset=UTF-8, application/jsonType and encoding of the body
Content-Dispositionattachment; filename="report.csv"Download instead of display
Content-Length2048Body size in bytes
Cache-Controlno-store, public, max-age=3600Caching rules
Location/dashboardRedirect target (with a 3xx status)
Retry-After120When to retry (with 429 or 503)

Security Headers

HeaderRecommended ValueProtects Against
Content-Security-Policydefault-src 'self' (adjusted to your site)XSS, loading untrusted scripts
X-Content-Type-OptionsnosniffBrowsers guessing content types
Content-Security-Policy: frame-ancestors 'self' (or X-Frame-Options: SAMEORIGIN)—Clickjacking (your page inside a hostile frame)
Referrer-Policystrict-origin-when-cross-originLeaking full URLs to other sites
Strict-Transport-Securitymax-age=31536000; includeSubDomainsDowngrade to insecure HTTP (send only over HTTPS)
<?php
function sendSecurityHeaders(): void
{
    header_remove('X-Powered-By');
    header("Content-Security-Policy: default-src 'self'; frame-ancestors 'self'");
    header('X-Content-Type-Options: nosniff');
    header('Referrer-Policy: strict-origin-when-cross-origin');

    if (($_SERVER['HTTPS'] ?? '') !== '' && $_SERVER['HTTPS'] !== 'off') {
        header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
    }
}

Security headers are often configured in the web server (Nginx, Apache) instead, so they apply to every response, including static files. A Content Security Policy must be tailored to the scripts, styles, and images your site actually uses; start in report-only mode (Content-Security-Policy-Report-Only) to avoid breaking pages.


Redirects

A redirect is a response with a 3xx status code and a Location header. The browser then requests the new URL.

<?php
header('Location: /dashboard.php', true, 303);
exit;

Always exit After a Redirect

header() only prepares the header; PHP keeps executing the script. Without exit, code after the redirect still runs — for example, saving data again or rendering a protected page whose content is sent to the client before the browser follows the redirect.

Choosing the Status Code

StatusNameMeaningTypical Use
301Moved PermanentlyThe URL has changed for good; browsers and search engines remember itOld URL → new URL, HTTP → HTTPS
302FoundTemporary redirectGeneric temporary redirect
303See Other"See the result with a GET request"After a form POST (Post/Redirect/Get)
307Temporary RedirectTemporary; keeps the method and bodyTemporarily moved API endpoint
308Permanent RedirectPermanent; keeps the method and bodyPermanently moved API endpoint

header('Location: ...') without a code sends 302 (unless a 3xx code was already set). Specify the code explicitly so the intention is clear.

Warning: Permanent redirects (301, 308) are cached by browsers, sometimes indefinitely. Use them only when the move is truly permanent, and test with temporary codes first.


Preventing Open Redirects

Redirecting to a URL taken from the request — such as login.php?return=/orders — is convenient after login. Without validation, an attacker can create a link like login.php?return=https://evil.example, and your trusted site sends users to a phishing page. This is an open redirect vulnerability. Allow only local paths:

<?php
function safeRedirectTarget(?string $target, string $fallback = '/'): string
{
    if ($target === null || $target === '' || preg_match('/[\x00-\x1F\x7F]/', $target)) {
        return $fallback;
    }

    // Must be a path on this site: starts with a single "/" (not "//" or "/\")
    if ($target[0] !== '/' || str_starts_with($target, '//') || str_starts_with($target, '/\\')) {
        return $fallback;
    }

    return $target;
}

$tests = ['/dashboard', '/orders?id=5', 'https://evil.example', '//evil.example', '/\\evil.example', null];

foreach ($tests as $target) {
    echo str_pad(var_export($target, true), 24) . '→ ' . safeRedirectTarget($target) . "\n";
}

Output

'/dashboard'            → /dashboard
'/orders?id=5'          → /orders?id=5
'https://evil.example'  → /
'//evil.example'        → /
'/\\evil.example'       → /
NULL                    → /

Why Each Rule Matters

  • Absolute URLs (https://...) point to other sites.
  • //evil.example is a protocol-relative URL: browsers treat it as https://evil.example.
  • Some browsers treat a backslash like a slash, so /\evil.example is also dangerous.
  • Control characters (such as newlines) are rejected; PHP's header() itself refuses headers containing newlines, which prevents header injection, but rejecting them early keeps the logic clear.

Use it as header('Location: ' . safeRedirectTarget($_GET['return'] ?? null), true, 303);.


Caching With ETags: 304 Not Modified

For responses that change rarely (such as a product catalog API), the server can send an ETag — a fingerprint of the content. On the next request, the browser sends it back in If-None-Match; if the content has not changed, the server replies 304 Not Modified without a body:

<?php
function respondWithEtag(string $body, ?string $ifNoneMatch): array
{
    $etag = '"' . hash('sha256', $body) . '"';

    if ($ifNoneMatch === $etag) {
        return [304, $etag, ''];
    }

    return [200, $etag, $body];
}

$body = json_encode(['products' => ['Pen', 'Notebook']]);

[$status, $etag] = respondWithEtag($body, null);
echo "First request: $status\n";

[$status] = respondWithEtag($body, $etag);
echo "Repeat request: $status\n";

// In a real endpoint:
// [$status, $etag, $content] = respondWithEtag($body, $_SERVER['HTTP_IF_NONE_MATCH'] ?? null);
// http_response_code($status);
// header("ETag: $etag");
// header('Cache-Control: no-cache');   // revalidate every time
// echo $content;

Output

First request: 200
Repeat request: 304

The browser reuses its cached copy when it receives 304, which saves bandwidth. Cache-Control: no-cache means "you may store it, but check with the server before using it", which is exactly what ETags support.


Common Mistakes

Output Before Headers

Effect: "Headers already sent" warnings; redirects and cookies fail.

Fix: Send headers first; remove stray whitespace and BOMs; omit closing ?>.

Missing exit After Location

Effect: Code continues running and may send protected content.

Fix: Always exit (or return from the controller) after redirecting.

Using 301 for Temporary Moves

Effect: Browsers keep redirecting even after the change is undone.

Fix: Use 302/303/307 for temporary redirects.

Redirecting to Unvalidated URLs

Effect: Open redirect vulnerabilities used in phishing.

Fix: Allow only local paths or an allow-list of domains.

Wrong or Missing Content-Type

Effect: Browsers misinterpret responses; JSON is shown as HTML or vice versa.

Fix: Set Content-Type (with charset) for every non-HTML response.


Best Practices

  • Set headers before any output, and keep pure PHP files free of closing tags.
  • Use precise status codes: 303 after form posts, 301/308 only for permanent moves.
  • Always stop execution after a redirect.
  • Validate redirect targets from user input.
  • Send security headers (or configure them in the web server), and use ETags or Cache-Control for cacheable responses.

Official reference: https://www.php.net/manual/en/function.header.php

0 Comments

Reviewed before they appear

No comments yet.

PHP
PHP Home PHP Switch PHP Match Alternative Syntax PHP Loops While Loop Do While Loop For Loop Foreach Loop Nested Loops Break Statement PHP Home PHP Intro PHP Install PHP Built-in Web Server PHP Syntax PHP Tags PHP and HTML PHP Comments PHP Multiline Comments PHP Variables Variables Scope PHP Casting Logical Operators Null Coalescing Operator Spaceship Operator Operator Precedence Continue Statement Modify Strings PHP If...Else...Elseif PHP If Modify Strings PHP If Operators PHP Functions Concatenate Strings PHP If...Else Slicing Strings User-defined Functions Slicing Strings PHP Shorthand if Escape Characters Default Argument Values PHP Nested if Pass by Reference String Interpolation Named Arguments PHP File Upload Variadic Functions Return Values Search in Strings Multibyte Strings Type Declarations in Functions PHP Numbers PHP Math PHP Floats PHP Random Numbers PHP Constants PHP Properties PHP Methods PHP Constructor Constructor Property Promotion PHP Cookies PHP Destructor PHP Access Modifiers Readonly Properties PHP Sessions PHP Inheritance Method Overriding PHP Filters The final Keyword CSRF Protection PHP Filters Advanced Session Security Secure Cookies PHP Magic Constants File Upload Security PHP Callback Functions PHP Operators Filesystem Security PHP JSON Arithmetic Operators Error Reporting in Production Hiding PHP Assignment Operators PHP Configuration (php.ini) Comparison Operators PHP Command Line (CLI) PHP Exceptions Increment / Decrement Operators Migrating to PHP 8 PHP Form Validation PHP Form Required PHP Form URL/E-mail Error Handling Form Input Sanitizing Escaping Output (htmlspecialchars) Custom Error Handler Checkbox, Radio and Select Inputs Sticky Forms PHP Form Complete PHP Date and Time Date Formatting Error Logging HTTP Headers and Redirects Output Buffering Sending Email PHP cURL Consuming REST APIs PHP Generators
Ask about this post
AI Ask about this post

Ask questions about HTTP Headers and Redirects and get answers drawn from it.

Signed-in readers only.