Caching: freshness, revalidation, and ResponseCache
◈ 5 cardsCache-Control and Expires as a freshness contract the server writes, the conditional GET that turns Last-Modified or ETag into a bodyless 304, and the ResponseCache hook Java gives you to answer a URLConnection locally.
The cheapest request is the one you never send
A cache is a store of responses you already have, plus a rule for deciding when one of them may be handed back instead of asking the origin server again. Every layer of the web keeps one: the browser on disk, a proxy at the edge of a campus, a CDN node, and — the one you can program — java.net.ResponseCache inside your own JVM.
HTTP does not leave the rule to the client. The server states the terms, in headers, and the client obeys them. That is the part to internalise: caching is a protocol feature negotiated in the response, not an optimisation a client is free to invent. A client that reuses a response for longer than the server permitted is not fast, it is wrong.
There are exactly two mechanisms and they solve different halves of the problem. Freshness removes the request entirely. Validation keeps the request but removes the body.
Freshness: Cache-Control and Expires
Cache-Control: max-age=600 says the response may be reused for 600 seconds after it was generated, with no contact of any kind. For those ten minutes a cache that honours the header answers without opening a socket. Other directives on the same header change the terms:
no-cache— store it, but revalidate before every reuse. It does not mean "do not cache".no-store— do not write it down at all. This is the one for a bank statement.must-revalidate— once stale it may never be served, not even when the origin is unreachable.public/private— may a shared proxy keep a copy, or only this one user's client?
Expires: Mon, 03 Aug 2026 12:17:55 GMT is the older HTTP/1.0 form of the same idea, an absolute date instead of a duration. Where both appear, max-age wins — a duration survives a client whose clock is wrong, and an absolute date does not.
Validation: the conditional GET
When the freshness window closes the stored copy is stale, which is not the same as wrong. Usually nothing has changed and downloading it again is pure waste, so the client asks a narrower question: has this changed since the copy I hold?
It asks using a validator kept from the previous response, and there are two.
Last-Modified: <date>arrives on the response; the client echoes it back asIf-Modified-Since: <date>. In Java,URLConnection.setIfModifiedSince(millis)writes that header for you.ETag: "9f3c-2b"is an opaque token the server assigns to one particular version of a resource; the client echoes it asIf-None-Match: "9f3c-2b". It is the stronger of the two because it depends on neither clocks nor the one-second granularity of an HTTP date.
If nothing has changed the server answers 304 Not Modified — a status line, a few headers, and no body at all. The client serves its stored copy and restarts the freshness clock from the new headers. One round trip and a couple of hundred bytes, instead of the whole page.
Java's hook: ResponseCache
java.net.ResponseCache is abstract and, exactly like CookieHandler in the next lesson, nothing is installed by default — out of the box every fetch through URL or URLConnection goes to the network. ResponseCache.setDefault(cache) installs one for the whole JVM and the protocol handler consults it from then on.
A subclass implements two methods, named for the two directions:
get(URI, String method, Map<String,List<String>> headers)returns aCacheResponsewhen you hold a usable copy andnullwhen you do not.CacheResponseexposesgetHeaders()andgetBody()— the stored headers and anInputStreamover the stored bytes.put(URI, URLConnection)returns aCacheRequestthe runtime will write the arriving response into, ornullto decline storing this one.CacheRequestsupplies anOutputStreamfromgetBody()and anabort()for a transfer that dies halfway.
Notice where the policy lives. The runtime calls get and uses whatever you hand back, so honouring max-age, no-store and the validators is your subclass's job. ResponseCache is a hook, not a cache. Per connection, setUseCaches(false) opts out; it is a configuration setter, so like every other one it must precede the first read or it throws IllegalStateException.
Worked example — the same page, fetched three times
Fetch http://data.example.org:8080/comp348/week3.html three times with a cache installed.
First fetch, cold. Nothing is stored, so a full request goes out and 200 OK comes back with 4,312 bytes, ETag: "9f3c-2b", a Last-Modified date and Cache-Control: max-age=600. put stores the headers and the body.
Second fetch, ninety seconds later. get finds the entry with 510 seconds of freshness left and answers from the store. No socket is opened at all. Nothing crossed the network, which is precisely why a caching bug is so hard to see: there is no exchange to inspect.
Third fetch, twenty minutes later. The entry is stale, so the client sends the same GET plus If-None-Match: "9f3c-2b" and gets 304 Not Modified — no body, and a fresh max-age=600. The stored copy is served and the clock restarts.
Three fetches, one body transferred. Now change one thing: someone edits the page between the second fetch and the third. The third exchange returns 200 OK with new bytes and a new ETag instead, and the cache replaces its entry. The client never had to know which of the two would happen — it asked a question that is correct either way, and that is the whole point of a validator.