<< Home

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.

⚠️ Treat input from clients as untrusted. Validate and sanitize before using values, and escape output for the correct context.

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);
}
💡 Use a front controller (one entry script) that routes all requests through a single file. Handle bootstrapping and shared middleware in one place.

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) ?? '';
⚠️ Avoid $_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);
  }
}
💡 Store only small identifiers in sessions. Load user profiles and permissions from your database per request to avoid stale copies.

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');
}
⚠️ Never trust the client provided filename. Generate your own safe name and store the original as metadata if needed.

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
💡 Set headers first, then output the body. If output buffering is on, flush with 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),
];
⚠️ Never trust $_SERVER['HTTP_X_FORWARDED_FOR'] directly. When behind a proxy, configure trusted proxies and read client IPs through your server or framework settings.