javascript 99 lines · 3 tabs

ETag-Based HTTP Caching in Express With 304 Not Modified Handling

Shared by codesnips Sep 2026
3 tabs
const crypto = require('crypto');

function computeStrongEtag(body) {
  const buf = Buffer.isBuffer(body) ? body : Buffer.from(String(body));
  const digest = crypto.createHash('sha1').update(buf).digest('base64');
  return '"' + digest + '"';
}

function etagCache() {
  return function (req, res, next) {
    if (req.method !== 'GET') {
      return next();
    }

    const originalSend = res.send.bind(res);

    res.send = function (body) {
      if (res.statusCode < 200 || res.statusCode >= 300 || body == null) {
        return originalSend(body);
      }

      const payload = typeof body === 'object' && !Buffer.isBuffer(body)
        ? JSON.stringify(body)
        : body;

      const etag = computeStrongEtag(payload);
      res.setHeader('ETag', etag);

      if (isEtagMatch(req.headers['if-none-match'], etag)) {
        res.status(304);
        res.removeHeader('Content-Type');
        res.removeHeader('Content-Length');
        return originalSend('');
      }

      return originalSend(payload);
    };

    next();
  };
}

module.exports = { etagCache, computeStrongEtag };
3 files · javascript Explain with highlit

This snippet shows how conditional HTTP caching works in Express by generating strong ETag values for GET responses and short-circuiting unchanged responses with 304 Not Modified. The idea is that once a client has already downloaded a representation, it can send back the previously received validator and the server can confirm the cache is still fresh without resending the body — saving bandwidth and rendering work while keeping correctness.

The etagCache middleware wraps the response by monkey-patching res.send. It only acts on safe, cacheable GET responses with a 2xx status, computes a content hash over the outgoing payload, and sets that hash as the ETag header. The helper computeStrongEtag normalises the body to a Buffer and derives a base64 SHA-1 digest, deliberately producing a strong validator (no W/ prefix) so byte-for-byte equality is asserted. This is the key trade-off: strong validators are precise but require hashing the full body, whereas weak validators would tolerate semantically-equivalent changes at lower cost.

Comparison is delegated to isEtagMatch, which parses the client's If-None-Match header, supports the * wildcard, splits multiple candidate tags, and normalises weak/strong forms before comparing. When a match is found, res.status(304) is set and the body is dropped by sending an empty payload — a 304 must not include a message body. Crucially, the middleware clears Content-Type and Content-Length on a 304 and preserves Cache-Control, matching the semantics the HTTP spec expects.

The products route tab shows the middleware applied at the router level, so every JSON response automatically gains an ETag. Because the hash is computed from the serialized response, no per-route bookkeeping is required; identical data always yields the same validator, and any change invalidates it.

A pitfall worth noting: this design hashes after the body is built, so it saves network transfer but not the database or serialization work that produced the body. For expensive computations, an application would pair this with an upstream key/value cache or a Last-Modified short-circuit. The pattern is best when payloads are moderate, responses are frequently re-requested, and correctness of freshness matters more than avoiding server-side compute.


Related snips

Share this code

Here's the card — post it anywhere.

ETag-Based HTTP Caching in Express With 304 Not Modified Handling — share card
Link copied