Headers, Status Codes and Redirects

Every HTTP response has a status code and headers before the body. PHP sets sensible defaults — 200 OK and Content-Type: text/html — but your scripts often need to change them: redirecting to another page, reporting "not found", returning JSON, or making the browser download a file.

Status codes

http_response_code() sets the status. The ones you'll use most:

CodeMeaningTypical use
200OKThe default
201CreatedAn API created a resource
204No ContentSuccess with nothing to return
301 / 302 / 303Moved permanently / Found / See otherRedirects
400Bad RequestInvalid input
401 / 403Unauthorized / ForbiddenNot logged in / not allowed
404Not FoundNo such page or record
405Method Not Allowede.g. GET on a POST-only endpoint
422Unprocessable ContentValidation failed (common in APIs)
500Internal Server ErrorSomething broke on the server
PHP article.php
<?php
$articles = [1 => 'Hello PHP', 2 => 'Sessions explained'];
$id = (int) ($_GET['id'] ?? 0);

if (!isset($articles[$id])) {
    http_response_code(404);
    echo "<h1>Article not found</h1>";
    exit;
}
echo "<h1>{$articles[$id]}</h1>";
HTTP
GET /article.php?id=2
GET /article.php?id=99
Responses
> GET /article.php?id=2
<h1>Sessions explained</h1>
> GET /article.php?id=99
< 404 Not Found
<h1>Article not found</h1>

Correct status codes matter: search engines won't index a "not found" page that returns 200, and API clients rely on them to detect errors.

Redirects

A Location header tells the browser to go to another URL. By default PHP sends 302 Found; pass a status code for others — 301 for a page that has moved permanently. Always exit afterwards:

PHP old-page.php
<?php
header('Location: /new-page.php', true, 301);
exit;
PHP go.php
<?php
$targets = ['docs' => 'https://www.php.net/docs.php', 'home' => '/'];
$to = $_GET['to'] ?? '';

if (!isset($targets[$to])) {
    http_response_code(400);
    exit('Unknown destination');
}
header('Location: ' . $targets[$to]);
exit;
HTTP
GET /old-page.php
GET /go.php?to=docs
GET /go.php?to=https://evil.example
Responses
> GET /old-page.php
< 301 Moved Permanently
< Location: /new-page.php
> GET /go.php?to=docs
< 302 Found
< Location: https://www.php.net/docs.php
> GET /go.php?to=https://evil.example
< 400 Bad Request
Unknown destination

Never redirect to a URL taken straight from user input — attackers use such "open redirects" to make phishing links look trustworthy. Map a short key to a known URL, as above, or only allow paths on your own site.

Content types

Content-Type tells the browser how to interpret the body. Send application/json for JSON, text/plain for plain text, text/csv for spreadsheets:

PHP api.php
<?php
header('Content-Type: application/json');

$product = ['id' => 7, 'name' => 'Desk lamp', 'price' => 34.5, 'tags' => ['lighting', 'office']];
echo json_encode($product);
HTTP
GET /api.php
Responses
> GET /api.php
< Content-Type: application/json
{"id":7,"name":"Desk lamp","price":34.5,"tags":["lighting","office"]}

Downloads

Content-Disposition: attachment makes the browser save the response as a file instead of displaying it — handy for exports:

PHP export.php
<?php
$rows = [['name', 'qty'], ['Pens', 10], ['Notebooks', 4]];

header('Content-Type: text/csv; charset=UTF-8');
header('Content-Disposition: attachment; filename="stock-report.csv"');

$out = fopen('php://output', 'w');
foreach ($rows as $row) {
    fputcsv($out, $row, escape: '');
}
HTTP
GET /export.php
Responses
> GET /export.php
< Content-Disposition: attachment; filename="stock-report.csv"
name,qty
Pens,10
Notebooks,4

php://output is a stream that writes to the response, so fputcsv can format the CSV correctly (quoting values with commas, for example).

"Headers already sent"

Headers must come before the body. Once any output has been sent — even a single space or a blank line before <?php — it's too late:

PHP late.php
<?php
echo "Hello";
header('Location: /elsewhere.php');
HTTP
GET /late.php
Responses
> GET /late.php
Hello<br />
<b>Warning</b>:  Cannot modify header information - headers already sent by (output started at late.php:2) in <b>late.php</b> on line <b>3</b><br />

The warning names the file and line where output started, which is where to look. Common culprits: whitespace after a closing ?> in an included file (one reason to omit closing tags), a byte-order mark at the start of a file, or debug echos. (On the web server, PHP formats warnings as HTML.)

Other useful headers

PHP
<?php
header('Cache-Control: no-store');                       // never cache (account pages)
header('Cache-Control: public, max-age=3600');           // cache for an hour
header('X-Content-Type-Options: nosniff');               // security: trust the Content-Type
header("Content-Security-Policy: default-src 'self'");   // security: limit where scripts load from
header('Access-Control-Allow-Origin: https://app.example.com');  // CORS for APIs

headers_sent() checks whether it's still possible to send headers, and headers_list() shows what's been set. To read the request headers sent by the browser, use $_SERVER (for example $_SERVER['HTTP_ACCEPT_LANGUAGE']) or getallheaders().

Try it yourself

Write a tiny JSON API endpoint that returns a product by ID, with a 404 and an error message when it doesn't exist.

Try it yourself · product.php
Edit the code, copy it into product.php, then start PHP's built-in web server with php -S localhost:8000 in that folder and open http://localhost:8000/product.php (PHP 8.5). How to install PHP