HTTP Fundamentals — Verbs, Status Codes, Headers, Caching
Interpret HTTP messages, methods and conditional response semantics; Separate cache freshness, validation, Vary and private data policies; Explain protocol versions, session state and bounded retry contracts.
Original publication date unrecordedUpdated
Lesson preparation & details
Level: intermediate
By the end, you should be able to
- Interpret HTTP messages, methods and conditional response semantics
- Separate cache freshness, validation, Vary and private data policies
- Explain protocol versions, session state and bounded retry contracts
Bring with you
Sources checked: . A source check does not change the publication or update date.
Editorial review: · What review means
In this article · 27 sections
Review boundary — 10 October 2026: HTTP contracts are checked against RFCs 9110–9114 and 5861. The exact request/response functions below run without a socket. The stdlib adapter and curl commands are references only: no server, CDN, external request demonstration or load benchmark was run.
Learning goal: Read HTTP/1.1 messages, choose method/status/cache semantics, distinguish transport versions, and reason about retries and session state.
Why this session exists
HTTP is used by many web APIs, proxies and object-store interfaces; it is not every microservice protocol. Understanding the protocol separates an application bug from framing, caching or retry behaviour. A successful transport response is not necessarily a successful business operation, and an unavailable cache does not automatically make the origin unreachable through the same failed edge.
You will be able to explain message lines, choose safe/idempotent methods, distinguish status families, design cache validators and directives, calculate an assumed hit-ratio effect, and compare HTTP/1.1, HTTP/2 and HTTP/3 without promising performance.
Prerequisites
Bring command-line basics, JSON and the ideas of a connection, request and response. TCP/TLS details can be studied separately; this lesson labels which message representation is specific to HTTP/1.1.
Optional pre-reading — original resource pointers. Video labels below identify the earlier suggestions, not verified recordings or viewing times. Search-result links do not establish authorship; no video content is endorsed here. Choose what helps before the intuition section; these are not required to finish the exercises.
- Video pointer: HTTP Crash Course & Exploration — optional curl/Postman orientation; recording content is unverified.
- Video search: HTTP/1.1 vs HTTP/2 vs HTTP/3 · Deep Dive.
- Video search: HTTP in 100 Seconds.
- Reading: MDN · HTTP Overview — orient yourself to methods, status codes and headers before reading the wire examples.
- Optional mnemonic: HTTP Cats · Status Codes — use the RFC-backed status section for semantics, not a picture as the protocol definition.
- Reading: HTTP Cache · Best Practices — a pointer for Cache-Control, ETag and the
immutableextension; compare freshness and validation with the lesson's RFC 9111 boundary cases. The extension is an optional reading topic, not implemented in the local fixture or newly verified here.
(a) Intuition · 5 min
A postal envelope is an analogy: the method states intent, the request target identifies what it concerns, fields carry metadata and content carries a representation. Host names the target authority, not the return address. HTTP/1.1 has textual start lines and headers; HTTP/2 and HTTP/3 use binary framing and pseudo-header fields, not the same literal text envelope.
The protocol is stateless in the sense that request semantics can be understood independently of prior connection history. Applications can still have users, cookies and server-side sessions. A cookie can carry only an opaque lookup key; it does not necessarily move all session state to the client or remove a shared-store dependency.
The pieces you'll use every day
Methods express intent; status codes describe response semantics; fields carry representation/cache/authentication metadata; bodies carry media-type-defined content. Use Authorization, not an invented generic Auth header. Accept states response preferences; Content-Type describes the actual content. Forms, multipart uploads, JSON and streaming content have different framing/media contracts.
The 30-year history in five events
Early HTTP developed from simple document retrieval through HTTP/1.0 and persistent HTTP/1.1. HTTP/2 introduced binary multiplexed streams; HTTP/3 maps semantics onto QUIC. For this lesson the normative current references are the June 2022 RFCs: 9110 semantics, 9111 caching, 9112 HTTP/1.1, 9113 HTTP/2, and 9114 HTTP/3. Protocol evolution changes framing and transport, not the definition of GET or 404. This avoids inferring a current deployment share from a historical milestone.
Request/response on the wire
GET /products/42 HTTP/1.1
Host: api.example.test
Accept: application/json
The request line contains method, target and version. Host identifies the authority. The blank line terminates fields. This GET has no content; GET content has no generally defined semantics and some intermediaries reject it.
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 9
Cache-Control: public, max-age=60
ETag: "product-v1"
{"id":42}The content is nine ASCII bytes. Field names are case-insensitive, not necessarily their values. Actual HTTP/1.1 line endings are CRLF. Length counts octets, not Unicode characters. A response can use other framing where permitted; not every message needs Content-Length. In particular 204 cannot contain content, and 304/HEAD have special metadata rules.
Figure text equivalent — labels and relationships
- Participant “Client”.
- Participant “DNS”.
- Participant “Edge”.
- Participant “Origin”.
- 1. solid: “Client” to “DNS” — Resolve authority if needed.
- 2. dotted: “DNS” to “Client” — Address information.
- 3. solid: “Client” to “Edge” — Establish or reuse transport and TLS.
- 4. solid: “Client” to “Edge” — GET public product representation.
- 5. alt start — Fresh reusable cache entry.
- 6. dotted: “Edge” to “Client” — Cached representation.
- 7. alt else — Needs origin response or validation.
- 8. solid: “Edge” to “Origin” — GET, possibly conditional.
- 9. dotted: “Origin” to “Edge” — 200 representation or 304 validation.
- 10. dotted: “Edge” to “Client” — Response according to cache policy.
- 11. alt end.
DNS may already be cached, connections may be reused, and HTTP/1.1 supports persistence too. Handshake cost depends on transport/TLS/network conditions; no fixed 20-ms DNS, 50–100-ms TLS or “two-thirds of round trips saved” result follows from the diagram. The pictured cache resource is public, not a user's private profile.
The verbs table you'll refer to for years
GET/HEAD: safe and idempotent intended semantics. Safety allows incidental logs/accounting; it forbids designing a read URL to perform a requested purchase/deletion. HEAD obtains GET metadata without response content. Automated fetching/prefetching depends on this contract.
POST: asks the resource to process content according to its semantics, often creation. It is not generally idempotent, but a specific application operation can be. Explicitly defined POST caching exists in HTTP semantics, although many caches do not implement it.
PUT: create/replace the target state, idempotent but unsafe. Repeating the same request has the same intended effect, not necessarily an identical status/body. Preconditions such as If-Match protect against lost updates; a response-loss retry can fail its precondition after the first successful write.
PATCH: applies a patch document whose media type defines operations. It is neither safe nor generally idempotent. Replacing a field can be idempotent; appending to an array may not be. JSON Patch and Merge Patch have standards, not an undefined partial-update convention.
DELETE: removes the association between target URI and current functionality. It is idempotent but unsafe. Repeating a successful delete may return 404 instead of 204 without violating idempotency. It does not promise immediate physical destruction across backups or a universal cancellation workflow.
Status codes, distilled
1xx provides interim information, such as 100 Continue. 2xx indicates success: 200 representation, 201 creation, 202 accepted processing not yet complete, 204 no content, 206 partial content. For 201, Location identifies the created primary resource when supplied; absent Location the request target identifies it. An API can make Location a stronger local requirement, but RFC 9110 does not require it in every 201.
3xx covers further action/redirect/cache semantics: 301 and 302 may change POST to GET for historical reasons; 307 and 308 preserve method/content; 303 directs retrieval of another resource; 304 is conditional validation, not a redirect to a different URL.
4xx includes 400 malformed request, 401 missing/invalid authentication credentials with a required authentication challenge, 403 understood but refused (not necessarily proof that authentication succeeded), 404 unavailable/undisclosed resource, 409 state conflict, 412 failed precondition, 415 unsupported media type, 422 unprocessable content and 429 rate limiting. Retry-After may accompany relevant errors; it can be delay-seconds or an HTTP date.
5xx includes 500 unexpected failure, 502 invalid upstream response, 503 temporary unavailability and 504 upstream timeout. Do not leak stack traces. Neither all 4xx require changing payload nor all 5xx are safe to retry: honour operation semantics, budget and server guidance.
Caching: the money layer
Freshness permits reuse without origin validation. A stale response may be validated with If-None-Match/ETag; 304 avoids retransmitting content but still consumes network/origin work. An ETag is an opaque representation validator; strong tags distinguish representation data, while weak tags can express semantic equivalence. Include representation variation and actual encoded bytes in a strong hash design.
Cache-Control: public, max-age=60, s-maxage=3600, stale-while-revalidate=86400 permits shared storage and sets a browser freshness lifetime of 60 seconds and shared-cache lifetime of 3,600 seconds. However, RFC 9111 §5.2.2.10 makes s-maxage incorporate proxy-revalidate: a shared cache must not reuse a stale response until successful origin validation. Do not infer a shared one-day stale allowance from this combined string. RFC 5861 allows bounded stale revalidation only where applicable policy permits it; stale-if-error is separate and is not an override of every prohibition. If the intended public policy is stale-while-revalidate without separate shared freshness, a simpler example is public, max-age=60, stale-while-revalidate=30, with no conflicting no-cache/must-revalidate/s-maxage restriction. Vendor-specific edge overrides require their own documented contract. Age, request directives and implementation affect reuse.
no-cache permits storage but requires successful validation before reuse. no-store prohibits compliant caching of the message; it is not encryption or retroactive deletion of previously stored copies. private excludes shared caches but can allow private browser caches. HTTPS and Set-Cookie alone do not provide a safe personalised cache policy. public can enable shared caching even in situations where authentication normally restricts it—never add it casually to private profiles.
Assume 10,000 requests/s, 99% fresh hits and no extra validation traffic. The origin receives 10,000 × (1−0.99) = 100 requests/s. If a separate stream of 500 background validations/s also reaches it, origin work is 600 requests/s instead; do not count validations already included among the original misses a second time. Neither ETag nor Cache-Control guarantees the hit ratio, staleness tolerance or monetary saving.
Age and conditional requests: work the boundary cases
A freshness lifetime is measured against current age, not a fresh countdown restarted whenever a response enters another cache. RFC 9111 §4.2.3 combines apparent age, received Age, response delay and local residence. In a synchronized-clock example, request time 100, response time 102, Date time 90, received Age 15 and current time 112 give apparent age 12, corrected received age 17, resident time 10, and current age 27 seconds. With a 60-second lifetime there are 33 seconds of freshness left. At age 60 it is stale, not fresh. These are seconds on an illustrative clock, not measured network timings.
# exact-function: cache_age (RFC 9111 arithmetic, finite nonnegative clock fixture)
def cache_age(request_time, response_time, date_value, age_value, now):
values = (request_time, response_time, date_value, age_value, now)
if any(type(v) is not int or v < 0 for v in values):
raise ValueError('integer nonnegative fixture seconds required')
if not request_time <= response_time <= now:
raise ValueError('invalid local clock order')
apparent = max(0, response_time - date_value)
corrected = age_value + response_time - request_time
return max(apparent, corrected) + now - response_timeConditional validation and write concurrency use the selected representation, after normal routing, request checks and authorisation. RFC 9110 §§13.1–13.2 requires strong comparison for If-Match and weak comparison for If-None-Match. If-Match takes precedence over If-Unmodified-Since; If-None-Match takes precedence over If-Modified-Since. If-Match is evaluated before If-None-Match when both occur. Conditions are not an authentication shortcut.
- Existing representation tagged
"v1", GET withIf-None-Match: W/"v1": 304, no content; retain applicable ETag, Vary, Date and cache fields that the corresponding 200 would send. - PUT with
If-Match: W/"v1": weak comparison is insufficient, so 412. PUT with a matching strong tag may proceed if all other checks pass. - PUT with
If-None-Match: *: create only if no current representation exists; otherwise 412. The wildcard tests existence, not a string literal ETag. - GET with failed If-Match and matching If-None-Match: 412 takes precedence, not 304. Matching If-None-Match on an unsafe method yields 412, not 304.
- If-Range is a range-selection condition, not a general write guard. Range support is optional and is not implemented in the local program.
The program below intentionally implements only If-None-Match on existing GET/HEAD representations and If-Match on PUT. It ignores the other conditional combinations; do not use it to infer that DELETE is concurrency-protected or create-only PUT is implemented. Its synthetic field dictionaries omit a real server clock/Date, and its entity-tag list parser is stricter than the complete HTTP list grammar. The adapter is not a complete conditional-request implementation. The cases above are the RFC contract, independently checked as decision fixtures, not a claim that the smaller handler implements them all.
Statelessness, method contracts and retries
Cookies and connection reuse solve different problems. A session ID can work over several connections; several users' stateless requests can share upstream connections without sharing identity. Affinity may help a particular stateful application, but it is not supplied by HTTP and does not repair arbitrary replica divergence.
Derive the retry problem: a timeout cannot distinguish “request never arrived” from “effect committed, response lost.” GET/HEAD rely on safe intent; PUT/DELETE rely on intended-effect idempotency. For a non-idempotent domain POST, introduce durable operation identity and reconciliation. A key header with no server-side atomic contract does not prevent duplicates. Limit attempts, back off with jitter, respect Retry-After and the end-to-end deadline. Three additional retries can create four attempts per original request; layered retries can multiply further. For example, three total attempts at each of two layers can produce nine downstream attempts. If Retry-After exceeds the remaining deadline, stop or hand the operation to a documented asynchronous recovery path rather than sleeping past the budget. Neither a 401 challenge nor an unchanged permission-denied 403 is fixed by blind retry; a payment timeout needs the same operation identity and reconciliation.
A self-describing envelope
Intermediaries use method, target, status and fields without understanding all application content. Some gateways do inspect bodies, so “nothing reads bodies” is false. Content-Type, encoding, framing and cache policy are protocol contracts, while business success and structured errors still need application semantics. Returning HTTP 200 for every failure can conceal useful transport-level signals, but GraphQL or batch responses can deliberately include per-field/item errors under a successful envelope.
Where session state lives
A server-side session store permits central revocation but depends on lookup availability, caches and replication. Revocation is not magically instant across every replica. Self-contained signed tokens permit local verification under trusted keys but expose readable claims unless encrypted and can remain valid until expiry; issuer/audience/expiry/algorithm and authorisation checks still matter. Short access tokens plus stateful refresh rotation trade lookup frequency against revocation delay and replay controls. Choose the revocation/failure contract first, not a universal rule that every large application should use one scheme.
(c) Hands-on · 25 min
Exact HTTP semantics without a server
The exact program handles synthetic requests in memory. It supports a deliberately small route/schema surface, UTF-8 JSON, strong byte-derived ETags, weak comparison for If-None-Match and strong comparison for If-Match. Private synthetic users are no-store; the public greeting route is the Vary demonstration. Its conditional subset and omitted Date/list-grammar cases are stated above. It is not a complete HTTP parser, authenticator or production service.
# exact-program: http_contract
import hashlib
import json
import re
USERS = {42: {'id': 42, 'name': 'alice'}}
NEXT_ID = 100
def encoded(value):
return json.dumps(value, ensure_ascii=False, sort_keys=True,
separators=(',', ':')).encode('utf-8')
def etag(body):
return '"' + hashlib.sha256(body).hexdigest() + '"'
def matches(header, current, weak):
if header is None:
return False
if header.strip() == '*':
return True # called only for an existing selected representation
token = r'(?:W/)?"[\x21\x23-\x7e\x80-\xff]*"'
if not re.fullmatch(r'\s*' + token + r'(?:\s*,\s*' + token + r')*\s*', header):
raise ValueError('invalid entity-tag list')
return any((tag.removeprefix('W/') == current if weak else tag == current)
for tag in re.findall(token, header))
def response(status, value=None, headers=None, head=False):
fields = {'Cache-Control': 'no-store', **(headers or {})}
body = b'' if value is None else encoded(value)
if status not in (204, 304):
fields.update({'Content-Type': 'application/json', 'Content-Length': str(len(body))})
return status, fields, b'' if head or status in (204, 304) else body
def handle(method, path, headers=None, raw=b''):
global NEXT_ID
h = {k.lower(): v for k, v in (headers or {}).items()}
if method in ('GET', 'HEAD'):
if path == '/greeting':
# Toy negotiation: first language token only, not full q-value negotiation.
lang = 'de' if h.get('accept-language', 'en').lower().startswith('de') else 'en'
value = {'greeting': 'Hallo' if lang == 'de' else 'Hello'}
fields = {'Cache-Control': 'public, max-age=60', 'Vary': 'Accept-Language',
'Content-Language': lang}
else:
m = re.fullmatch(r'/users/([0-9]+)', path)
if not m or int(m[1]) not in USERS:
return response(404, {'error': 'not_found'}, head=method == 'HEAD')
value, fields = USERS[int(m[1])], {'Cache-Control': 'no-store'}
tag = etag(encoded(value)); fields['ETag'] = tag
try:
unchanged = matches(h.get('if-none-match'), tag, weak=True)
except ValueError:
return response(400, {'error': 'bad_condition'}, head=method == 'HEAD')
return response(304 if unchanged else 200, None if unchanged else value,
fields, method == 'HEAD')
if method not in ('POST', 'PUT', 'DELETE'):
return response(405, {'error': 'method_not_allowed'},
{'Allow': 'GET, HEAD, POST, PUT, DELETE'})
m = re.fullmatch(r'/users/([0-9]+)', path)
if (method == 'POST' and path != '/users') or (method != 'POST' and not m):
return response(404, {'error': 'not_found'})
uid = int(m[1]) if m else None
if method == 'DELETE':
if uid not in USERS:
return response(404, {'error': 'not_found'})
del USERS[uid]
return response(204)
if len(raw) > 4096:
return response(413, {'error': 'too_large'})
if h.get('content-type', '').split(';')[0].strip().lower() != 'application/json':
return response(415, {'error': 'json_required'})
try:
value = json.loads(raw.decode('utf-8'))
except (UnicodeDecodeError, json.JSONDecodeError):
return response(400, {'error': 'invalid_json'})
if (not isinstance(value, dict) or set(value) != {'name'} or
not isinstance(value['name'], str) or not 1 <= len(value['name']) <= 100):
return response(422, {'error': 'invalid_name_or_fields'})
existed = uid in USERS
if method == 'PUT' and 'if-match' in h:
try:
ok = existed and matches(h['if-match'], etag(encoded(USERS[uid])), weak=False)
except ValueError:
return response(400, {'error': 'bad_condition'})
if not ok:
return response(412, {'error': 'precondition_failed'})
if method == 'POST':
uid = NEXT_ID; NEXT_ID += 1
USERS[uid] = {'id': uid, 'name': value['name']}
return response(200 if method == 'PUT' and existed else 201, USERS[uid],
{'Location': f'/users/{uid}'})
if __name__ == '__main__':
a = handle('GET', '/greeting', {'Accept-Language': 'de'})
assert a[0] == 200 and a[2] == b'{"greeting":"Hallo"}'
assert handle('GET', '/greeting', {'Accept-Language': 'de',
'If-None-Match': 'W/' + a[1]['ETag']})[0] == 304
assert handle('HEAD', '/greeting')[2] == b''
assert handle('POST', '/users', {'Content-Type': 'application/json'}, b'[]')[0] == 422
assert handle('DELETE', '/users/42')[0] == 204
assert handle('DELETE', '/users/42')[0] == 404
print('HTTP contract examples passed without opening a socket')A stdlib adapter reference
For an authorised future local demonstration, save the exact program as http_contract.py and use this adapter. It was not started. BaseHTTPRequestHandler dispatches methods; the pure function supplies responses. This adapter explicitly closes connections and accepts Content-Length request framing only, rejecting transfer coding. It deliberately does not claim complete HTTP/1.1 support or production security.
# service-reference: stdlib HTTP adapter (not run)
from http.server import BaseHTTPRequestHandler, HTTPServer
from http_contract import handle
class Handler(BaseHTTPRequestHandler):
protocol_version = 'HTTP/1.1'
def dispatch(self):
self.close_connection = True
if self.headers.get('Transfer-Encoding'):
self.send_error(501, 'transfer coding unsupported in this demo'); return
lengths = self.headers.get_all('Content-Length', [])
if len(lengths) > 1:
self.send_error(400, 'duplicate Content-Length'); return
try:
length = int(lengths[0]) if lengths else 0
except ValueError:
self.send_error(400, 'invalid length'); return
if not 0 <= length <= 4096:
self.send_error(413); return
raw = self.rfile.read(length)
if len(raw) != length:
self.send_error(400, 'short content'); return
status, fields, body = handle(self.command, self.path, dict(self.headers), raw)
self.send_response(status)
for key, value in fields.items():
self.send_header(key, value)
self.send_header('Connection', 'close')
self.end_headers()
self.wfile.write(body)
do_GET = do_HEAD = do_POST = do_PUT = do_DELETE = dispatch
if __name__ == '__main__':
with HTTPServer(('127.0.0.1', 8080), Handler) as server:
try:
server.serve_forever()
except KeyboardInterrupt:
passReference curl calls exercise 200, 304, 201, 422 and 204. Copy the actual ETag from the first response rather than the original hardcoded value that never matched the hash:
curl -v -H 'Accept-Language: de' 'http://127.0.0.1:8080/greeting'
curl -v -H 'Accept-Language: de' -H 'If-None-Match: "ACTUAL_ETAG"' 'http://127.0.0.1:8080/greeting'
curl -v -H 'Content-Type: application/json' -d '{"name":"bob"}' 'http://127.0.0.1:8080/users'
curl -v -H 'Content-Type: application/json' -d '{}' 'http://127.0.0.1:8080/users'
curl -v -X DELETE 'http://127.0.0.1:8080/users/42'Anatomy of the script
encoded defines the bytes that the strong validator hashes and the length measures. matches supports wildcard and comma-separated tags, including weak comparison for conditional GET; a weak tag cannot satisfy strong If-Match. response emits no body for HEAD/204/304 and preserves validator/cache/Vary metadata on 304. handle rejects bad JSON, unknown fields and client-supplied IDs rather than merging arbitrary input into stored users. It is single-process fixture state with no durable idempotency or concurrent transaction guarantee.
Vary exercise and cache-key oracle
Request /greeting in English and German. A simplistic cache keyed only by URL can return the first stored greeting for both; keying on URL plus the request's Accept-Language value separates them. The response includes Vary: Accept-Language, and the selected representation's ETag differs too. A direct pair of curl requests to an origin does not reproduce a CDN cache collision; the local test uses an explicit cache dictionary negative control. No Netflix incident or twenty-minute outage is asserted.
Cookie reference commands can demonstrate a jar in a future authorised environment, but a cookie jar alone does not prove server-side session storage or load-balancer affinity. The protocol explanation above is independent of running public httpbin requests.
(d) Production reality · 15 min
Edge failure, replica lag and retry amplification
These are hypothetical failure analyses, not embellished Cloudflare/GitHub postmortems. A failed edge can block both cached and origin traffic; an origin fallback needs a tested routing/capacity path. Stale-if-error helps only if the serving cache is functioning and policy permits reuse. Replica lag can violate read-after-write expectations even when every HTTP message is valid; consistency tokens or suitable read routing belong to the application/storage design. Retrying all failures immediately can amplify overload; use bounded budgets, jitter, operation identity and deadline-aware recovery.
HTTP/1.1, HTTP/2 and HTTP/3
HTTP/1.1 supports persistent connections and pipelining, but pipelined responses remain ordered. HTTP/2 multiplexes streams over TCP, removing HTTP/1.1 response-order blocking across streams while TCP packet loss can stall delivery for the connection. HTTP/3 uses QUIC streams and avoids that TCP-wide delivery dependency. Individual stream ordering, flow control, congestion, scheduling and application dependencies still cause delays. “No head-of-line blocking of any kind” and “always faster on mobile” are not guarantees.
Where this shows up in the rest of the plan
REST uses resource/method semantics; GraphQL defines an application execution envelope; gRPC maps messages/status onto HTTP/2. Authentication, cache policy, retries and observability still need explicit contracts in each. Not every token travels in Authorization, and not every service metric is an HTTP status.
(e) Recall + stretch · 10 min
Why is GET safe but POST not generally safe?
GET requests read-oriented semantics; incidental logging can still occur. POST can request effects. Prefetching and automatic retrieval must not trigger a purchase or deletion hidden behind GET.
Which of PUT, POST, PATCH and DELETE are idempotent?
PUT/DELETE by specified intended effect; POST/PATCH only under an additional operation contract. Status/body equality is not required. Preconditions and intervening updates still affect retry decisions.
Distinguish 401, 403 and 404 for admin access.
401 requires an authentication challenge for missing/invalid credentials; 403 refuses the understood request; 404 can mean absent or deliberately undisclosed. A 403 is not universal proof of successful login.
What does the public max-age/s-maxage/stale-while-revalidate string mean?
Private-cache freshness 60 seconds and shared-cache freshness 3,600 seconds. The string also carries a stale-revalidation extension, but RFC 9111 makes s-maxage prohibit stale shared reuse until successful validation. It therefore does not promise a one-day shared stale window, hit rate or origin-outage availability; stale-if-error is separate.
What does ETag save?
Conditional validation can yield 304 without content, saving representation transfer while still doing validation work. A validator identifies the selected representation; weak/strong comparison and Vary matter.
One sentence per HTTP version?
1.1 uses text framing with persistence and ordered pipelined responses. 2 multiplexes binary streams over TCP. 3 maps semantics onto QUIC and avoids TCP-wide loss blocking, not all delays.
How should order creation respond?
POST to the collection can return 201 with the created resource and Location under the API contract. Async processing may return 202 instead; acceptance is not completion.
no-cache versus no-store?
Store-but-validate before reuse versus do-not-store in compliant caches. Neither is an authorisation mechanism or retroactive erasure guarantee.
Why Vary?
It identifies request fields affecting representation selection. Without suitable cache-key variation a cache can mix language/encoding variants. Sensitive personalised data additionally needs a safe private/no-store policy.
Stretch: design a retry-safe payment contract.
POST with one key per logical payment, scoped by authenticated account and operation, a canonical payload fingerprint, durable effect/result linkage and documented retention. Replay the original recorded status/body/needed headers; reject same-key different-payload conflicts. Concurrent/in-progress and crash recovery need explicit semantics. On ambiguous timeout reuse/query the same identity with bounded backoff, not a fresh key. Stripe documents replay including original errors and pruning keys only after at least 24 hours; this does not mean every API returns 200 on duplicates or guarantees no double charge forever.
Failure exercise and worked answer
204 followed by 404 on DELETE can preserve the same intended deleted state. It is not an idempotency violation. The application may retain operation identity if clients need to distinguish a replay from a never-existing resource.
Explain-out-loud test
Explain session identity versus transport lifetime; contrast PUT/PATCH/POST; and design separate cache policies for public greetings and private user data.
What comes next
Next: S056 · REST API Design — resources, versioning, idempotency. REST design makes these semantics a domain contract. Hub: The 6-Month Learning Plan.
Study route: intuition → visual walkthrough → hands-on → production failure analysis → recall. Use the section times as planning cues, not a completion guarantee; pause to work through the checks. Optional pre-reading is separate.
Sources and review boundary
RFC 9110, 9111, 9112, 9113, 9114, RFC 5861 stale controls, Stripe idempotency. Exact passages and local test hashes are retained. Socket framing, TLS, browsers, actual proxies/CDNs and production security remain unperformed integration checks.
Pause / Recall / Apply
Can you explain it without the page?
Close the example. Reconstruct the core idea, then change one assumption. Mark complete when you’re ready; you can always undo it.
Stored in this browser only. No account, no sync. Clearing browser data removes your record.