Identify the representation that may be reused
Two responses with the same URL and 200 status can have different bodies. In an international portal, Accept-Language may select PT or EN; at another endpoint, identity may determine personalized data. Start diagnosis by recording the actual request, received representation, and response policy. Vary communicates request dimensions relevant to variant selection. It does not turn restricted content into public content. Key selection, permission to store, and access rights are distinct decisions that must form a coherent contract. Check each explicitly during acceptance.
Read validation without inventing a document
A client may store the body and ETag and send If-None-Match on its next read. A 304 confirms reuse of the corresponding representation; it does not supply a new empty JSON document. Consumer code must retain the body and update applicable metadata. If it requests EN with the PT validator, the origin evaluates the selected EN representation. The lab uses different ETags for different bodies and demonstrates a complete response when the variant changes. It supports one entity tag rather than the full list or wildcard grammar.
Separate storage from freshness
Unqualified no-cache permits a storage strategy with mandatory validation before reuse. No-store prohibits storing the response in applicable scopes and is not evidence that earlier copies disappeared. Unqualified private prevents shared storage while allowing a private cache to satisfy the remaining rules. When investigating an exposed report, correct future policy and address existing copies and observed impact. A header does not replace authentication, authorization, encryption, or incident handling. Do not use a short lifetime as justification for sharing personalized data. Document the affected cache locations.
Define behavior when the origin fails
Freshness budget depends on calculated age, not when someone reopened the screen. In the simplified example, a 120-second lifetime minus 95 seconds of age leaves 25 seconds. When a stale response requires must-revalidate, losing the origin connection does not allow silently presenting it as current. Discuss with the service owner whether a historical or degraded mode exists, which decisions it excludes, and how age is displayed. Do not change the guarantee without making the new requirement explicit and documenting its acceptance criteria.
Run the local laboratory
Save this lesson’s code as run.py and execute python3 run.py --output evidence.json in a temporary directory. The program opens HTTP only on 127.0.0.1 at a system-assigned port and closes the server afterward. Compare PT and EN, validate a response, change its revision, and observe the new body. Incorrect-key and storage-decision examples are in-memory models separate from HTTP transport. X-Lab-Tenant is synthetic context, not authentication. There is no TLS, actual CDN, complete HTTP cache, banking request, or distributed durability in this laboratory.
Accept observable results
During the workshop, predict outcomes before reading evidence. Explain why 304 does not replace the body, why ETags differ, and where the URL-only key fails. Repeat requests in both orders to avoid a demonstration that works only with an empty cache. To apply the design in a project, identify actual intermediaries and plan authorized checks with distinct variants and identities. Local evidence teaches the reasoning but does not automatically approve a product configuration or an institution’s policy. Keep fictional examples separate from operational procedures.
"""Original bounded HTTP teaching fixture, Python 3.13.1 reference runtime.
Run: python3 run.py --output evidence.json
Loopback and synthetic data only. Not a production server, complete HTTP cache,
authentication system, database snapshot engine or durable cursor store.
"""
import argparse
import copy
import hashlib
import http.client
import json
import platform
import threading
import uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import parse_qs, urlencode, urlsplit
def encode(value):
return json.dumps(value, sort_keys=True, separators=(',', ':')).encode
class State:
def __init__(self):
self.reset
def reset(self):
self.rows = [{'id': n, 'tenant': 'A', 'amount': n * 10, 'state': 'open'} for n in range(1, 7)]
self.rows += [{'id': 101, 'tenant': 'B', 'amount': 700, 'state': 'open'}]
self.tokens = {}
self.clock = 100
self.allowed = {'A', 'B'}
self.revision = 1
def handler_for(state):
class Handler(BaseHTTPRequestHandler):
protocol_version = 'HTTP/1.1'
def log_message(self, *args):
pass
def send(self, status, value=None, headers=None):
body = b'' if value is None else encode(value)
self.send_response(status)
for key, val in (headers or {}).items:
self.send_header(key, val)
if status!= 304:
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', str(len(body)))
self.end_headers
if body:
self.wfile.write(body)
def do_GET(self):
parsed = urlsplit(self.path)
query = parse_qs(parsed.query)
if parsed.path in ('/catalogue', '/profile', '/secret'):
# Exact pt/en labels only; not a general Accept-Language parser.
lang = 'pt' if self.headers.get('Accept-Language') == 'pt' else 'en'
tenant = self.headers.get('X-Lab-Tenant', '')
if parsed.path!= '/catalogue' and tenant not in state.allowed:
return self.send(403, {'error': 'synthetic-context-denied'})
policy = {'/catalogue': 'public, no-cache', '/profile': 'private, max-age=60', '/secret': 'no-store'}[parsed.path]
value = {'language': lang, 'label': 'Fundos' if lang == 'pt' else 'Funds', 'revision': state.revision}
if parsed.path!= '/catalogue':
value['tenant'] = tenant
tag = '"' + hashlib.sha256(encode(value)).hexdigest + '"'
headers = {'ETag': tag, 'Vary': 'Accept-Language', 'Cache-Control': policy, 'Content-Language': lang}
# Single entity-tag comparison only, no lists or wildcard support.
condition = self.headers.get('If-None-Match', '').removeprefix('W/')
if condition == tag:
return self.send(304, headers=headers)
return self.send(200, value, headers)
if parsed.path!= '/list':
return self.send(404, {'error': 'not-found'})
tenant = self.headers.get('X-Lab-Tenant', '')
if tenant not in state.allowed:
return self.send(403, {'error': 'synthetic-context-denied'})
mode = query.get('mode', ['keyset'])[0]
filter_ = query.get('filter', ['open'])[0]
if mode not in ('offset', 'keyset', 'snapshot') or filter_ not in ('open', 'all'):
return self.send(400, {'error': 'unsupported-query'})
cursor = query.get('cursor', [''])[0]
previous = state.tokens.get(cursor) if cursor else None
if cursor and previous is None:
return self.send(400, {'error': 'unknown-cursor'})
if previous:
if any(previous[k]!= v for k, v in [('tenant', tenant), ('mode', mode), ('filter', filter_)]):
return self.send(400, {'error': 'cursor-scope-mismatch'})
if state.clock >= previous['expires']:
return self.send(410, {'error': 'cursor-expired'})
live = sorted([copy.deepcopy(r) for r in state.rows if r['tenant'] == tenant and (filter_ == 'all' or r['state'] == filter_)], key=lambda r: r['id'])
frozen = previous['snapshot'] if previous and mode == 'snapshot' else copy.deepcopy(live)
if mode == 'keyset':
candidates = [r for r in live if r['id'] > (previous['last'] if previous else 0)]
index = 0
else:
candidates = frozen if mode == 'snapshot' else live
index = previous['index'] if previous else 0
page = candidates[index:index + 2]
more = len(candidates) > index + len(page)
next_ = ''
if more:
next_ = uuid.uuid4.hex
state.tokens[next_] = {'tenant': tenant, 'mode': mode, 'filter': filter_, 'expires': state.clock + 10, 'index': index + len(page), 'last': page[-1]['id'], 'snapshot': frozen if mode == 'snapshot' else None}
return self.send(200, {'items': page, 'next_page_token': next_}, {'Cache-Control': 'no-store'})
return Handler
def run:
state = State
server = ThreadingHTTPServer(('127.0.0.1', 0), handler_for(state))
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start
checks = []
def check(name, actual, expected):
if actual!= expected:
raise AssertionError((name, actual, expected))
checks.append({'name': name, 'actual': actual, 'expected': expected, 'passed': True})
def request(path, headers=None):
client = http.client.HTTPConnection('127.0.0.1', server.server_port, timeout=3)
try:
client.request('GET', path, headers=headers or {})
response = client.getresponse
body = response.read
return response.status, {k.lower: v for k, v in response.getheaders}, body
finally:
client.close
def listing(mode, cursor='', tenant='A', filter_='open'):
status, _, body = request('/list?' + urlencode({'mode': mode, 'cursor': cursor, 'filter': filter_}), {'X-Lab-Tenant': tenant})
return status, json.loads(body)
def ids(data):
return [r['id'] for r in data['items']]
try:
pt = request('/catalogue', {'Accept-Language': 'pt'})
en = request('/catalogue', {'Accept-Language': 'en'})
check('localized representations', [json.loads(pt[2])['label'], json.loads(en[2])['label']], ['Fundos', 'Funds'])
check('vary declared', pt[1]['vary'], 'Accept-Language')
check('variant validators differ', pt[1]['etag']!= en[1]['etag'], True)
validated = request('/catalogue', {'Accept-Language': 'pt', 'If-None-Match': pt[1]['etag']})
check('not modified response', [validated[0], len(validated[2])], [304, 0])
validated_body = pt[2] if validated[0] == 304 else validated[2]
check('cached body retained after validation', json.loads(validated_body)['label'], 'Fundos')
weak = request('/catalogue', {'Accept-Language': 'pt', 'If-None-Match': 'W/' + pt[1]['etag']})
check('weak comparison for GET', weak[0], 304)
cross = request('/catalogue', {'Accept-Language': 'en', 'If-None-Match': pt[1]['etag']})
check('other language needs representation', [cross[0], json.loads(cross[2])['language']], [200, 'en'])
state.revision = 2
changed = request('/catalogue', {'Accept-Language': 'pt', 'If-None-Match': pt[1]['etag']})
check('changed representation returns body', [changed[0], json.loads(changed[2])['revision']], [200, 2])
# Deliberately wrong and corrected in-memory cache keys; not an HTTP proxy.
bad = {'/catalogue': pt[2]}
good = {('/catalogue', 'pt'): pt[2], ('/catalogue', 'en'): en[2]}
check('URL-only key reproduces wrong language', json.loads(bad['/catalogue'])['language'], 'pt')
check('variant key selects English', json.loads(good[('/catalogue', 'en')])['language'], 'en')
profile = request('/profile', {'X-Lab-Tenant': 'A'})
secret = request('/secret', {'X-Lab-Tenant': 'A'})
# Conservative policy selection for these unqualified fixtures only.
def can_store(headers, shared):
directives = {x.strip for x in headers['cache-control'].split(',')}
return 'no-store' not in directives and not (shared and 'private' in directives)
check('private disallows shared storage', can_store(profile[1], True), False)
check('private may allow private storage', can_store(profile[1], False), True)
check('no-store disallows both scopes', [can_store(secret[1], True), can_store(secret[1], False)], [False, False])
check('no-cache may store before validation', can_store(pt[1], True), True)
state.reset
_, first = listing('offset')
state.rows = [r for r in state.rows if r['id']!= 1]
_, second = listing('offset', first['next_page_token'])
check('offset deletion skips an unseen item', [ids(first), ids(second)], [[1, 2], [4, 5]])
state.reset
_, first = listing('keyset')
state.rows = [r for r in state.rows if r['id']!= 1]
_, second = listing('keyset', first['next_page_token'])
check('keyset survives earlier deletion', [ids(first), ids(second)], [[1, 2], [3, 4]])
for row in state.rows:
if row['id'] == 3:
row['amount'] = 999
_, replay = listing('keyset', first['next_page_token'])
check('keyset replay is not a snapshot', replay['items'][0]['amount'], 999)
state.reset
_, first = listing('snapshot')
token = first['next_page_token']
for row in state.rows:
if row['id'] == 3:
row['amount'] = 999
_, second = listing('snapshot', token)
check('frozen snapshot retains old value', second['items'][0]['amount'], 30)
check('cursor cannot change tenant scope', listing('snapshot', token, tenant='B')[0], 400)
check('cursor cannot change filter scope', listing('snapshot', token, filter_='all')[0], 400)
check('cursor cannot change mode', listing('keyset', token)[0], 400)
check('unknown cursor rejected', listing('snapshot', 'not-issued')[0], 400)
state.allowed.remove('A')
check('current permission checked before snapshot', listing('snapshot', token)[0], 403)
state.allowed.add('A')
state.clock += 10
check('synthetic cursor expiry', listing('snapshot', token)[0], 410)
state.reset
status, page = listing('snapshot')
collected = ids(page)
while page['next_page_token']:
status, page = listing('snapshot', page['next_page_token'])
collected += ids(page)
check('complete traversal and terminal token', [collected, page['next_page_token'], status], [[1, 2, 3, 4, 5, 6], '', 200])
check('tenant B has separate population', ids(listing('keyset', tenant='B')[1]), [101])
# Pure synthetic arithmetic, separate from the actual HTTP observations.
check('remaining freshness from supplied age', max(0, 120 - 95), 25)
check('non-unique sort key drops tie', [r['id'] for r in [{'id': 1, 'time': 9}, {'id': 2, 'time': 9}, {'id': 3, 'time': 10}] if r['time'] > 9], [3])
tied = [{'id': 1, 'time': 9}, {'id': 2, 'time': 9}, {'id': 3, 'time': 10}]
check('composite boundary retains unread tie', [r['id'] for r in tied if (r['time'], r['id']) > (9, 1)], [2, 3])
return {'runtime': platform.python_version, 'transport': 'Actual loopback HTTP/1.1; synthetic identity, state and clock.', 'scope': 'Selected origin responses and pagination behavior; in-memory key and policy models are separate. No TLS, real authentication, proxy implementation, database isolation or production effects.', 'checks': checks, 'passed': len(checks), 'runnerSha256': hashlib.sha256(Path(__file__).read_bytes).hexdigest}
finally:
server.shutdown
server.server_close
thread.join(timeout=3)
if __name__ == '__main__':
parser = argparse.ArgumentParser
parser.add_argument('--output')
args = parser.parse_args
result = json.dumps(run, indent=2) + '\n'
if args.output:
Path(args.output).write_text(result)
else:
print(result, end='')
A cache returns Fundos to an EN request because it stored only /catalogue. The origin can already return Funds; correct variant selection is missing.
Common pitfalls
Treating 304 as empty JSON; confusing no-cache with no-store; treating Authorization as an absolute cache ban; resetting Age to hide staleness.
Related topics: Requests and outcomes · Caching and pagination · Concurrent changes and verifiable recovery
A correct cache reuses the permitted representation for the right request within its validation and freshness policy.
Reference: HTTP Caching · HTTP semantics RFC9110; OpenAPI3.2.1; selected primary standards and provider contracts consulted2026-09-30