# HTTP caching: select the right copy before reusing it

Original LockedIn Labs workshop · 45 minutes. Python 3.11+ standard library.
Read a request, a response status and JSON first. The runner starts its own
short-lived HTTP origin on a random 127.0.0.1 port, sends actual GETs and closes
it. No packages, account, credentials, outside requests or database are used.
All instruction text and source revisions are synthetic.

You will distinguish selecting a representation, retaining its bytes and
validating a later use. The [Caching 101 workshop](/training/guides/caching)
covers application keys, current authority, expiry and late publication. This
workshop adds HTTP evidence; it does not replace those application checks.

## 1. Separate the address from the representation

A **representation** is the particular response content selected for a resource.
Our `/localized` address can return English or French. `Accept-Language` is the
request header selecting exactly `en` or `fr` here; language preference lists,
quality values and regional negotiation are deliberately outside the fixture.
The origin responds with `Vary: Accept-Language`, identifying that selecting
request field. A URL-only key can therefore find the wrong saved copy.

**Cache-Control** describes response caching conditions. The unqualified
`no-cache` requires validation before later reuse; `no-store` prohibits cache
retention. `Vary: *` prevents a reusable request match. These are separate rules,
not alternative spellings of the same instruction. Our small client elects not
to retain star responses. [HTTP Caching, RFC 9111](https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1)
is the primary source for these distinctions.

An **ETag** is an opaque representation validator. Our origin invents tags such
as `"en-r1"`; a real client copies its service's tag instead of guessing it.
`If-None-Match` carries that validator on a conditional GET. A **304 Not Modified**
response carries no new representation body; it can validate saved bytes rather
than supply a replacement document. [HTTP Semantics, RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html#section-15.4.5)
defines this response. It differs from a new 200 response containing JSON.

## 2. Follow the selected body across the boundary

Predict two things separately: whether a request reaches the origin, and which
representation the client returns. A cache result is not necessarily an HTTP
response that just crossed the network. In the report, `network_status: null`
means this request used a local fresh entry. It does not mean the origin
returned a missing or invented status.

The visual compares six independently reset cases from the runner. It is an
authored explanation; running the Python file supplies your own HTTP evidence.

## 3. Run six actual HTTP comparisons

Download and read [the runner](/templates/fde-http-cache.py). Keep an untouched
reference and write predictions before opening its JSON output:

```sh
python3 --version
python3 fde-http-cache.py > cache-original.json
python3 fde-http-cache.py --case url-only-variant --first-language fr > cache-reversed.json
```

Each case has its own origin and empty cache. The driver performs requests in a
deliberate sequential order; this is not concurrent traffic, a CDN or a browser
cache test. Successful completion records `listener_closed: true` only after
the owned server thread stops and its listener socket closes.

| Case | Network status sequence | Actual result to explain |
| --- | --- | --- |
| `url-only-variant` | 200, local reuse, local reuse | en / fr / en requests all receive English; only one origin request |
| `vary-aware` | 200, 200, local reuse | en / fr / en receive their respective language; two stored entries |
| `no-cache-revalidation` | 200, 304, 200 | Stored r1 reused after validation, then replaced with r2 bytes |
| `no-store` | 200, 200 | Both requests reach origin; no reusable entry retained |
| `vary-star` | 200, 200 | No request match; this client retains no entry |
| `orphan-304` | 304 | Deliberately faulty upstream response; result withheld with no body invented |

Open the written ledger after inspecting the diagram:

```text
url-only-variant, request 2:
  requested_language: fr
  source: stored-fresh
  network_status: null
  representation.language: en
  origin_exchanges count: 1

vary-aware, request 2:
  requested_language: fr
  source: origin-body
  network_status: 200
  representation.language: fr
  origin_exchanges count: 2 after all three requests

no-cache-revalidation, request 2:
  If-None-Match: "en-r1"
  origin response: 304, body_bytes: 0
  source: validated-stored
  representation: saved English revision 1

no-cache-revalidation, request 3:
  If-None-Match: "en-r1"
  origin response: 200, ETag: "en-r2", new JSON bytes
  source: origin-body
  representation: English revision 2
```

Read `origin_exchanges` beside `steps`, not just a hit count. The origin actually
received a conditional GET in the 304 step. Zero response-body bytes is not zero
network traffic, zero header bytes, a measured latency gain or an avoided model
call. The fixture doesn't use a model.

In `stored_entries`, compare each body's language with its saved
`request_headers`. The guarded selection keeps both together. The deliberately
broken selection ignores only Vary; it still checks age and no-cache. A young
entry can therefore be the wrong representation without being expired.

## 4. Change the first request without changing its meaning

Compare `cache-original.json` with `cache-reversed.json`. The latter starts in
French, requests English next, then French again. The URL-only cache now returns
French to the English request. Reversing the order changes the wrong answer,
not the violated rule. The requested language remains the independent target.

Run the guarded counterpart with the same changed input:

```sh
python3 fde-http-cache.py --case vary-aware --first-language fr > cache-guarded-fr.json
python3 fde-http-cache.py --case no-cache-revalidation --first-language fr > cache-validated-fr.json
```

Both selected variants still match the requests. In the validation case, tags
are now `"fr-r1"` and `"fr-r2"`. Never compare a tag's apparent digits as a
universal ordering rule. The origin owns the validator and its selected content.
The driver advances the source only after the second request; no HTTP request
from the learner mutates an outside service.

## 5. Diagnose before changing cache policy

The validate route returns `no-cache, max-age=600`. Read the client's no-cache
branch before its age comparison. The second request validates despite being
young. The 304 has the same authored ETag, Vary and Cache-Control; the client
keeps the old body and refreshes its age record. The third request reaches the
origin again and receives new bytes after the source revision changes.

The no-store route also advertises max-age, but the client stores no reusable
entry. The report still shows the synthetic content delivered to its caller.
No-store is not a promise that application reports, screenshots, logs or copies
are erased. Do not put private records in this exercise.

The star case also retains zero entries, by this client's conservative choice.
Do not conclude that the standard universally forbids storing a Vary-star
response: its important reuse boundary is the absence of a matching request.

The orphan case intentionally violates the expected upstream exchange: it
returns 304 to an unconditional request when no body has been stored. The client
returns `representation: null` with reason `no_matching_stored_representation`.
A receiver should investigate that exchange rather than treat an empty document
as a successful response. The fixture withholds on other unsupported statuses.

Read `LocalCache.request`. Its supported scope is deliberately narrow: GET,
these fixed paths/directives, one exact language header, complete JSON bodies,
and unchanged validation metadata. It estimates local age conservatively from
Date, observed response delay and monotonic residence; it never sleeps for a
TTL experiment. It implements no general cache parser, shared authorization,
cookies, TLS, partial responses, redirects, stale-on-error or browser service
worker. Real 304 metadata updates and other HTTP cases need a complete cache
implementation and receiving-environment tests. Use maintained infrastructure
for production; do not deploy Python's teaching server or this small cache.

## 6. Receive the cache contract as a delivery pod

Use three roles, rotating the receiver on another run:

- **Builder:** retain command, runtime, request headers, network observations,
  returned representation and source-change ordering. Show the exact broken rule.
- **Receiver:** reverse the first language without author takeover. Locate the
  wrong copy, both guarded entries, and a bodyless 304 with its matching body.
- **Application/platform owner:** name who owns representation selection,
  intermediary configuration, current authorization and unsupported responses.
  Keep an unresolved receiving-environment experiment with a bounded stop rule.

Each person keeps an individual record; a shared correct explanation cannot
establish every participant's contribution:

```text
Learner / run / exact command:
Prediction before execution:
Requested language / returned language / source:
Original request headers and origin exchange reference:
Observed status / validator / network body bytes:
Stored entry and original selecting fields:
My contribution / assistance received:
Changed input / prediction before rerun / observed evidence:
Receiving action actually attempted / assistance:
Unresolved behavior / accountable owner / next bounded experiment:
```

For an enterprise document API, identify the actual browser, proxy, gateway and
application cache boundaries. Use synthetic content and an approved receiving
route. Capture actual response headers and chosen representation through that
route, then test changed preferences, source revisions and unavailable origin
behavior. Language matching establishes neither tenant isolation nor current
access. A provider's private/public setting is not a substitute for the
application's resource authorization contract.

## 7. Defend it in an interview

**Explain:** Can a fresh hit be wrong? Use request 2 in the broken variant case,
including requested and returned language and the absent second origin request.

**Debug:** Why does no-cache return stored bytes after a 304? Locate the exact
conditional request, zero new body bytes and the retained r1 representation.
Then explain why the third call receives r2 bytes.

**Design:** Specify a cache contract for a scoped enterprise source. Name the
cached object, selecting fields, validation/retention policy, actual intermediary,
authorization owner, unsupported-response behavior and receiving evidence. State
what this local experiment leaves untested before recommending a rollout.

Primary references checked 28 October 2026:
[HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111.html),
[HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110.html), and
[Python HTTP client](https://docs.python.org/3.12/library/http.client.html).
The runner, comparisons, diagram, interview prompts and receiving exercise are
original instruction derived from the observed fixture behavior.
