Chapter 14: HTTP Essentials
HTTP underpins every PHP web application. Understanding the request lifecycle, the server interface, and the correct use of headers, cookies, sessions, and JSON lets you build predictable applications that behave well behind proxies and browsers.
Request and Response Lifecycle
A PHP script usually runs behind a web server and PHP SAPI (Apache, FPM, CLI server). The server parses the HTTP request, sets environment variables and superglobals, runs your script, and returns the bytes you echo along with headers you emit.
Tracing the flow from client to server
A browser opens a TCP connection, sends a request line, headers, and possibly a body. PHP exposes these details via $_SERVER and input streams. Your script reads data, produces content, sets headers, and the SAPI flushes a response to the client.
<?php
// Request line and headers as seen by PHP
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$uri = $_SERVER['REQUEST_URI'] ?? '/';
$agent = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
header('Content-Type: text/plain; charset=UTF-8');
echo "Method: $method\nURI: $uri\nUser-Agent: $agent\n";
Reading the request body safely
For JSON and other raw payloads, read php://input. For form posts, rely on $_POST and $_FILES. Avoid reading php://input multiple times since it is a stream.
<?php
if (($_SERVER['CONTENT_TYPE'] ?? '') === 'application/json') {
$raw = file_get_contents('php://input');
$data = json_decode($raw, true);
}
Forms and Superglobals
HTML forms submit key value pairs through query strings or request bodies. PHP provides $_GET, $_POST, and $_REQUEST for these values, and filter_input offers explicit validation filters.
Submitting and handling a simple form
Use the correct method and enctype attributes. Read values from $_POST or $_GET, then validate and escape output for HTML.
<form method="post" action="/subscribe.php">
<label>Email: <input type="email" name="email" required></label>
<button>Subscribe</button>
</form>
<?php
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if ($email) {
// save and respond
echo htmlspecialchars($email, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
} else {
http_response_code(422);
echo 'Invalid email address';
}
Using filter_input with validation and defaults
filter_input reads from the request and applies a filter in one step. Provide options to set ranges and default values.
<?php
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT,
['options' => ['min_range' => 1, 'default' => 1]]
);
$q = filter_input(INPUT_GET, 'q', FILTER_UNSAFE_RAW) ?? '';
$_REQUEST since it merges sources in a configurable order. Read from the specific bag that matches your form method.
Cookies and Sessions
Cookies allow small per browser key value storage. Sessions associate a server side record with a client by sending a session identifier cookie. Use secure flags and regenerate identifiers after privilege changes.
Setting and reading cookies correctly
Set cookies before output starts. Use secure, httponly, and an appropriate samesite value to reduce risk.
<?php
setcookie(
'prefs',
json_encode(['theme' => 'dark']),
[
'expires' => time() + 60 * 60 * 24 * 30,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]
);
$prefs = [];
if (isset($_COOKIE['prefs'])) {
$prefs = json_decode($_COOKIE['prefs'], true) ?: [];
}
Starting sessions and managing identifiers
Call session_start before reading or writing $_SESSION. Regenerate the identifier after login to prevent fixation, and set strict cookie flags in session_set_cookie_params.
<?php
session_set_cookie_params([
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
session_start();
if (!isset($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
function rotate_session_id(): void {
if (session_status() === PHP_SESSION_ACTIVE) {
session_regenerate_id(true);
}
}
File Uploads and Validation
Uploading files uses multipart/form-data. PHP collects metadata in $_FILES. Always verify upload success, check size, validate content type with finfo, and move the file to controlled storage.
Building an upload form with constraints
Specify enctype="multipart/form-data" and provide input constraints with accept and max limits on the server.
<form method="post" enctype="multipart/form-data">
<label>Avatar: <input type="file" name="avatar" accept="image/*"></label>
<button>Upload</button>
</form>
Validating and moving uploaded files
Use UPLOAD_ERR_OK to verify status, filesize and finfo to inspect content, then call move_uploaded_file.
<?php
if (!isset($_FILES['avatar']) || $_FILES['avatar']['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
exit('Upload failed');
}
$tmp = $_FILES['avatar']['tmp_name'];
$size = filesize($tmp);
if ($size < 1 || $size > 2 * 1024 * 1024) {
http_response_code(413);
exit('File too large');
}
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($tmp);
$allowed = ['image/png' => 'png', 'image/jpeg' => 'jpg', 'image/webp' => 'webp'];
if (!isset($allowed[$mime])) {
http_response_code(415);
exit('Unsupported media type');
}
$ext = $allowed[$mime];
$dest = __DIR__ . '/uploads/' . bin2hex(random_bytes(16)) . ".$ext";
if (!move_uploaded_file($tmp, $dest)) {
http_response_code(500);
exit('Could not store file');
}
Headers, Redirects, and Status Codes
HTTP headers describe metadata such as content type, caching, authentication, and redirection. Choose status codes that reflect the outcome accurately, and send headers before the body.
Setting response headers explicitly
Send Content-Type with a charset when returning text. For caching, control validators and max age. For authentication, include the correct scheme and realm text.
<?php
header('Content-Type: text/html; charset=UTF-8');
header('Cache-Control: no-store');
header('X-Content-Type-Options: nosniff');
// Example bearer challenge with placeholder
header('WWW-Authenticate: Bearer realm="api", error="invalid_token", error_description="…"', true, 401);
Issuing redirects safely with Location
Use a 303 for redirect after POST, since it instructs the client to fetch the new location with GET. Validate any user supplied return URL against an allow list.
<?php
$next = '/account';
http_response_code(303);
header('Location: ' . $next);
exit;
Choosing accurate status codes
Use a small, consistent subset for clarity. The following list covers common scenarios in web applications.
| Code | Name | Typical use |
| 200 | OK | Successful request with body |
| 201 | Created | New resource created |
| 204 | No Content | Successful request without body |
| 303 | See Other | Redirect after POST to a GET URL |
| 400 | Bad Request | Malformed input or missing parameters |
| 401 | Unauthorized | Authentication required, send challenge |
| 403 | Forbidden | Authenticated but not allowed |
| 404 | Not Found | Resource does not exist |
| 409 | Conflict | State conflict such as duplicate |
| 422 | Unprocessable Content | Validation failed for a well formed request |
| 500 | Internal Server Error | Unexpected failure on the server |
flush()) only after headers are established.
Working with JSON
JSON is the most common wire format for APIs. Set correct content types, encode with the right options, and handle decoding errors explicitly.
Sending JSON responses with safe encoding
Use JSON_UNESCAPED_UNICODE for readability and JSON_THROW_ON_ERROR to catch encoding issues. Always send application/json with a charset.
<?php
header('Content-Type: application/json; charset=UTF-8');
$payload = [
'ok' => true,
'items' => [['id' => 1, 'name' => 'Café']],
];
try {
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
http_response_code(500);
echo json_encode(['ok' => false, 'error' => 'Encoding failed']);
}
Receiving JSON requests from clients
Check the content type, read the raw body, decode with exceptions, and validate required fields. Return 400 when payloads are malformed.
<?php
if (stripos($_SERVER['CONTENT_TYPE'] ?? '', 'application/json') !== 0) {
http_response_code(415);
exit('Unsupported media type');
}
try {
$data = json_decode(file_get_contents('php://input'), true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
http_response_code(400);
exit('Invalid JSON');
}
if (!isset($data['title']) || !is_string($data['title'])) {
http_response_code(422);
exit('Missing or invalid title');
}
Avoiding common pitfalls with numbers and dates
Large integers can exceed 64 bit limits in JavaScript clients, and naive date strings cause timezone confusion. Use strings for identifiers, and prefer RFC 3339 for timestamps.
<?php
$order = [
'id' => (string) 20251104123456789, // serialized as string to preserve precision
'created_at' => (new DateTime('now', new DateTimeZone('UTC')))->format(DateTime::RFC3339),
];
$_SERVER['HTTP_X_FORWARDED_FOR'] directly. When behind a proxy, configure trusted proxies and read client IPs through your server or framework settings.