expresshttpbrowser-cachehttp-cachinghttp-status-code-304

Behaviour of express / browser as it relates to cache headers and 304 response codes


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?


Solution

  • 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.