HTTP Fundamentals: How the Web Talks

Understand HTTP as the request/response protocol underlying all web (and PHP web) development: what a request and a response contain, how methods, headers, bodies, status codes, and content types fit together, and the roles of redirects, caching, cookies, and authentication at a conceptual level before diving into each in detail.

Every PHP web application — no matter how it's built, from a single index.php to a full framework — exists to answer HTTP requests with HTTP responses. Before writing a line of routing or controller code, it pays to understand the protocol itself: what arrives when a browser or API client hits your server, what you're expected to send back, and the vocabulary (methods, status codes, headers, caching, cookies, authentication) that every subsequent PHP lesson in this module builds on. This lesson is the map; the following three lessons zoom into methods, status codes, and headers respectively.

The request/response cycle

HTTP (HyperText Transfer Protocol) is a text-based, stateless, request-response protocol. A client (a browser, curl, a mobile app, another server) opens a connection to a server and sends an HTTP request; the server processes it and sends back exactly one HTTP response, then (in the common case) the connection is reused or closed. 'Stateless' means the server does not, by itself, remember anything between one request and the next — each request must carry everything needed to understand it, which is precisely why mechanisms like cookies and authentication tokens exist: they're how statelessness gets bridged into a stateful-feeling experience like a logged-in session.

A request has four parts: a request line (method + path + HTTP version, e.g. GET /products?page=2 HTTP/1.1), a set of headers (metadata as key: value pairs), a blank line, and an optional body (present for methods like POST that submit data). A response mirrors this shape: a status line (HTTP version + status code + reason phrase, e.g. HTTP/1.1 200 OK), headers, a blank line, and an optional body (the HTML, JSON, image bytes, or other payload being returned).

• Request line: method + path + version — e.g. GET /login HTTP/1.1 • Request headers: Host, Accept, Cookie, Content-Type, Authorization, ... • Request body: present for POST/PUT/PATCH, absent for GET/HEAD/DELETE by convention • Status line: version + status code + reason phrase — e.g. HTTP/1.1 404 Not Found • Response headers: Content-Type, Set-Cookie, Cache-Control, Location, ... • Response body: HTML, JSON, an image, or empty depending on the status/method

Methods, status codes, and content types at a glance

The HTTP method tells the server what kind of action the client wants performed: GET to retrieve, POST to submit/create, PUT/PATCH to replace/update, DELETE to remove, plus the less-visible HEAD and OPTIONS. Lesson 6.2 covers all seven in depth, including which are 'safe' (read-only) and which are 'idempotent' (repeating them has the same effect as doing them once).

The status code in the response tells the client how the request turned out, grouped into five classes by their first digit: 1xx informational, 2xx success, 3xx redirection, 4xx client error, 5xx server error. Lesson 6.3 walks through every commonly used code in each class. Alongside the status code, the Content-Type header (and matching Accept header on the request) tells each side how to parse the body — text/html, application/json, image/png, and so on — a mismatch here is one of the most common sources of 'why is my API returning garbage' bugs. Lesson 6.4 covers Content-Type and every other important header in depth.

Redirects, caching, cookies, and authentication — the big picture

A redirect (a 3xx status code paired with a Location header) tells the client 'the resource you want is actually somewhere else, go there instead' — used after a successful form POST to send the browser to a new page (avoiding a duplicate submission if the user refreshes), or to send old URLs to new ones permanently. Caching lets a response say 'you (or an intermediate proxy/CDN) can reuse this exact response for a while without asking me again,' controlled mainly through the Cache-Control header and validated with ETag/Last-Modified plus the 304 Not Modified status — this is how the web serves billions of unchanged images and scripts without re-transmitting them on every page load.

Cookies are small pieces of data the server asks the browser to store (via Set-Cookie in a response) and automatically send back on every subsequent request to that domain (via Cookie in the request) — this is the primary mechanism that turns stateless HTTP into something that feels like a continuous logged-in session. Authentication is the broader question of 'who is making this request, and are they allowed to do what they're asking' — commonly implemented via a session cookie referencing server-side state, or via a bearer token sent in the Authorization header for APIs. Each of these four topics gets much more depth as this module progresses, but every one of them is, underneath, just a specific pattern of headers riding on top of the plain request/response cycle described above.

A raw HTTP request and response, side by side

This is literally what travels over the TCP connection (or TLS-wrapped connection for HTTPS) — PHP's $_SERVER, $_GET, getallheaders(), and php://input are all just structured views onto this raw request. The blank line between headers and body is mandatory and is how the parser on either end knows headers have ended.

GET /api/products/42 HTTP/1.1
Host: shop.example.com
Accept: application/json
User-Agent: curl/8.4.0
Cookie: session_id=abc123

(no body for a GET request)

--- server responds ---

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=60
ETag: "v42-rev7"
Content-Length: 58

{"id":42,"name":"Wireless Mouse","price":19.99}

Code Examples

PHP automatically parses the raw HTTP request line and headers into $_SERVER superglobal entries, prefixing header names with HTTP_ and upper-casing/underscoring them. This is the foundation every PHP router and framework builds routing decisions on top of. Expected result: GET /api/products/42?ref=home shop.example.com application/json http_response_code() sets the status line, header() adds response headers, and anything echoed after headers are sent becomes the response body. This mirrors exactly the raw HTTP response shown in the syntax block above — PHP is just a convenient way to build that text.

header('Content-Type: application/json; charset=utf-8');

This single script reads the method and path (routing), reads the request body via php://input (bodies), branches on method (methods), validates and returns 422 on bad input (status codes), sets a cookie for session tracking (cookies/authentication groundwork), and always sets Content-Type (content types) — every concept from this lesson working together in miniature. file_get_contents('php://input') is how PHP exposes a raw, non-form-encoded request body (like a JSON POST body) — $_POST only auto-populates for application/x-www-form-urlencoded or multipart/form-data submissions.

<?php
// Inspecting the incoming request PHP just received
echo $_SERVER['REQUEST_METHOD'], "\n";   // e.g. GET
echo $_SERVER['REQUEST_URI'], "\n";      // e.g. /api/products/42?ref=home
echo $_SERVER['HTTP_HOST'], "\n";        // e.g. shop.example.com
echo $_SERVER['HTTP_ACCEPT'] ?? 'n/a', "\n";  // e.g. application/json
<?php
// Sending a minimal, well-formed HTTP response by hand
http_response_code(200);
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: max-age=60, public');

echo json_encode(['id' => 42, 'name' => 'Wireless Mouse', 'price' => 19.99]);
<?php
// A tiny front controller demonstrating the full cycle end to end
$method = $_SERVER['REQUEST_METHOD'];
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);

if ($method === 'GET' && $path === '/api/ping') {
    http_response_code(200);
    header('Content-Type: application/json');
    echo json_encode(['status' => 'ok']);
} elseif ($method === 'POST' && $path === '/api/login') {
    $body = json_decode(file_get_contents('php://input'), true);
    if (empty($body['username'])) {
        http_response_code(422);
        header('Content-Type: application/json');
        echo json_encode(['error' => 'username is required']);
        exit;
    }
    setcookie('session_id', bin2hex(random_bytes(16)), ['httponly' => true, 'samesite' => 'Lax']);
    http_response_code(200);
    header('Content-Type: application/json');
    echo json_encode(['status' => 'logged in']);
} else {
    http_response_code(404);
    header('Content-Type: application/json');
    echo json_encode(['error' => 'not found']);
}

The HTTP request/response cycle

Stateless by default: each arrow-pair is a self-contained exchange, and anything that must persist across exchanges (login state, a shopping cart) rides in cookies or tokens carried within the headers.

CLIENT (browser, curl, app)                 SERVER (PHP)
  ----------------------------                 ------------
        |                                             |
        |  1. Request:                                |
        |     GET /products/42 HTTP/1.1                |
        |     Host: shop.example.com                   |
        |     Accept: application/json                 |
        |  ------------------------------------------> |
        |                                             |
        |                                     2. PHP reads
        |                                        $_SERVER,
        |                                        $_GET, headers,
        |                                        runs application
        |                                        logic, queries DB
        |                                             |
        |  3. Response:                               |
        |     HTTP/1.1 200 OK                          |
        |     Content-Type: application/json           |
        |     {"id":42,...}                            |
        | <------------------------------------------  |
        |                      
…

Important Rules

HTTP is stateless — nothing is remembered between requests unless the request itself carries it (cookies, tokens, query params).

Every response should set an accurate Content-Type so the client knows how to parse the body.

Status codes communicate outcome; don't rely on body content alone to signal success or failure — set the right code too.

Bodies are conventional, not enforced: GET/HEAD/DELETE requests typically carry no body even though the protocol technically permits one on some of them.

Redirects, caching, cookies, and authentication are all just structured uses of headers layered on top of the same basic request/response cycle.

Where this shows up

Every PHP framework's front controller (Symfony's public/index.php, Laravel's public/index.php) is fundamentally this same request-in, response-out cycle, just with a router, middleware stack, and templating layer wrapped around it. Debugging a 'the browser shows raw JSON as text instead of formatting it' or 'my API client can't parse the body' complaint almost always comes back to inspecting the raw request/response headers with browser devtools or curl -v — the exact structure covered in this lesson.

<?php echo json_encode(['id' => 42]); header('Content-Type: application/json'); // too late!

<?php header('Content-Type: application/json'); echo json_encode(['id' => 42]);

Headers must be sent before any body output. Once echo (or any output, even accidental whitespace before <?php) has run, PHP has already sent the default headers and started the body — a later header() call triggers a 'headers already sent' warning and is silently ignored.

What does it mean that HTTP is 'stateless'? A. The server crashes if too many requests arrive at once B. The server does not remember anything between separate requests unless the request itself carries that context C. Responses never include a body D. Only GET requests are allowed

B. The server does not remember anything between separate requests unless the request itself carries that context — Statelessness means each request is handled independently; anything that needs to persist across requests (login state, a cart) must be carried explicitly, typically via cookies or tokens in the headers.

In PHP, why must header() calls happen before any echo or other output? A. PHP requires alphabetical ordering of function calls B. HTTP headers must be transmitted before the response body, and once output starts, PHP has already sent the headers C. header() is deprecated and only works before output for legacy reasons D. It doesn't matter; PHP buffers everything automatically in all configurations

B. HTTP headers must be transmitted before the response body, and once output starts, PHP has already sent the headers — The HTTP response format requires headers before the body. PHP sends headers automatically as soon as the first byte of output occurs, so any header() call after that point is too late and only triggers a warning.

Which pair of headers is most directly responsible for turning stateless HTTP into a logged-in session feel? A. Accept and Content-Type B. Host and Origin C. Set-Cookie (response) and Cookie (request) D. Cache-Control and ETag

C. Set-Cookie (response) and Cookie (request) — The server issues a cookie via Set-Cookie; the browser automatically re-sends it via the Cookie header on every subsequent request to that domain, which is how session/login state persists across otherwise-stateless requests.

Write a PHP script for the path /api/echo that reads the incoming Content-Type header and, if it's not exactly 'application/json', responds with status code 415 and a JSON error body; otherwise it should read the JSON request body with php://input, and echo it back with status 200 and Content-Type: application/json.

<?php $contentType = $_SERVER['CONTENT_TYPE'] ?? ''; if (stripos($contentType, 'application/json') !== 0) { http_response_code(415); header('Content-Type: application/json'); echo json_encode(['error' => 'Content-Type must be application/json']); exit; } $body = file_get_contents('php://input'); http_response_code(200); header('Content-Type: application/json'); echo $body;

Check $_SERVER['CONTENT_TYPE'] (or use getallheaders()['Content-Type'] ?? ''), and remember http_response_code() must be called before any output.