Headers and Redirects

Headers, Redirects and Post/Redirect/Get

header() adds a response header and http_response_code() sets the status. Answer a successful POST with a page and Reload re-sends it, storing a second review. Post/Redirect/Get answers with 303 See Other instead, so the browser fetches a harmless GET. A failed POST re-renders with 422.

The review controller (review.php)PHP
<?php
declare(strict_types=1);
require __DIR__ . '/lib.php';
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    [$review, $errors] = validateReview($_POST);
    if (!$errors) {
        $line = json_encode($review + ['at' => date(DATE_ATOM)], JSON_THROW_ON_ERROR);
        file_put_contents(__DIR__ . '/reviews.jsonl', "$line\n", FILE_APPEND | LOCK_EX);
        header("Location: /review.php?thanks={$review['book']}", response_code: 303);
        exit;
    }
    http_response_code(422);                      // re-show the form with its errors
}
$thanks = filter_input(INPUT_GET, 'thanks', FILTER_VALIDATE_INT);
require __DIR__ . '/form.php';
Output
$ curl -s -i -L -d 'book=102&rating=5&email=ann@example.com&tags[]=clear' $U/review.php \
    --data-urlencode 'body=Clear chapters on joins and indexes.' | grep -E '^(HTTP|Loc|<p)'
HTTP/1.1 303 See Other
Location: /review.php?thanks=102
HTTP/1.1 200 OK
<p>Thank you for reviewing book 102.</p><form method="post" action="/review.php">

With -L, curl 3,008 followed the 303 with a GET, as a browser does. A bare Location sends 302, ambiguous after a POST; exit so nothing else runs. Headers must precede output: Ubuntu 225 's 4 KB output_buffering hides a stray echo until output passes 4,096 bytes. A late.php that printed 4,400 bytes first got Cannot modify header information - headers already sent by (output started at /home/dev/shop/late.php:2); headers_sent() reports that spot. Flash messages need a session.