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:
| Code | Meaning | Typical use |
|---|---|---|
| 200 | OK | The default |
| 201 | Created | An API created a resource |
| 204 | No Content | Success with nothing to return |
| 301 / 302 / 303 | Moved permanently / Found / See other | Redirects |
| 400 | Bad Request | Invalid input |
| 401 / 403 | Unauthorized / Forbidden | Not logged in / not allowed |
| 404 | Not Found | No such page or record |
| 405 | Method Not Allowed | e.g. GET on a POST-only endpoint |
| 422 | Unprocessable Content | Validation failed (common in APIs) |
| 500 | Internal Server Error | Something broke on the server |
<?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>";
GET /article.php?id=2 GET /article.php?id=99
> 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 header('Location: /new-page.php', true, 301); exit;
<?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;
GET /old-page.php
GET /go.php?to=docs
GET /go.php?to=https://evil.example
> 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 header('Content-Type: application/json'); $product = ['id' => 7, 'name' => 'Desk lamp', 'price' => 34.5, 'tags' => ['lighting', 'office']]; echo json_encode($product);
GET /api.php
> 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 $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: ''); }
GET /export.php
> 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 echo "Hello"; header('Location: /elsewhere.php');
GET /late.php
> 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 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.