I am trying to understand the behaviour of cache headers and response codes.
Initially I have an express application that looks like this:
app.get("/users-a", async (req, res) => {
await new Promise((res) => setTimeout(res, 3000));
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Cache-Control", `public, max-age=10`);
res.json(users);
});
Essentially we simulate our 'database lookup' taking 3000ms, and return a response after that. The response cache headers are public, max-age=10
.
In a browser, I now repeatedly click a button to make this request, and what we see is that the first request takes 3000ms, and subsequent are immediate - they use the browser's disk cache, but then after 10 seconds the 3000ms request is made again.
Note that the response contains the header:
Etag: W/"1a-Q1ZzAew6rLbf4JEZbVarkQiSXKo"
This Etag is automatically applied by express, see this comprehensive discussion here:
how does a etag work in expressjs
And the requests contain the header:
If-None-Match: W/"1a-Q1ZzAew6rLbf4JEZbVarkQiSXKo"
This all makes sense to me so far.
I now want to simulate the browser, now having stale data, making that request and receiving a 304 status code - instructing it to use its existing content.
We amend the application like this:
app.get("/users-b", async (req, res) => {
res.setHeader("Access-Control-Allow-Origin", "*");
const ifNoneMatch = req.headers["if-none-match"];
if (ifNoneMatch && etags.has(ifNoneMatch)) {
res.sendStatus(304);
} else {
await new Promise((res) => setTimeout(res, 3000));
res.setHeader("Cache-Control", `public, max-age=${10}`);
res.json(users);
const etagValue = res.getHeader("etag");
etags.add(etagValue);
}
});
This is very crude, but essentially we store the etag that express generates into a set, and then if receive a request with the if-none-match header, then immediately return a 304, without a response body - the client already has it.
What I would expect to see is that in the browser we would see a 304 response coming through. Certainly the express code does execute down the res.sendStatus(304)
path.
But what we see instead is that in the network tab the browser has a 200 response, with the response body. The response doesn't take 3000ms suggesting that the cache behaviour is working correctly.
I guess my question here is - is this just how browsers behave with respect to 304 status codes - it converts it to a 200 response without showing you?
Per Kevin Henry's comment, this appears to be a bug in Chromium.
I've tested in Chrome 129 and it appears to be behaving correctly.
Moral of the story: if you do encounter browser behaviour that seems odd, it's worth checking if the same behaviour also occurs on other major browsers.