From 331ba978c376f555023408efe8a1503d4aaba30f Mon Sep 17 00:00:00 2001 From: Nazmul Hossain Date: Wed, 12 Aug 2026 10:06:04 +0600 Subject: [PATCH 1/3] fix: send the immutable Cache-Control directive as its own field-line --- index.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 1655053..b03e6cf 100644 --- a/index.js +++ b/index.js @@ -747,10 +747,14 @@ SendStream.prototype.setHeader = function setHeader (path, stat) { var cacheControl = 'public, max-age=' + Math.floor(this._maxage / 1000) if (this._immutable) { - cacheControl += ', immutable' + // Send the `immutable` directive as its own field-line so the + // `public, max-age=` value can still be matched against the + // HTTP/3 QPACK static table. Caches combine repeated field-lines, so + // the effective directives are unchanged. + cacheControl = [cacheControl, 'immutable'] } - debug('cache-control %s', cacheControl) + debug('cache-control %o', cacheControl) res.setHeader('Cache-Control', cacheControl) } From e6237cc6c16f5ff5fbf58335e88123647457e9ac Mon Sep 17 00:00:00 2001 From: Nazmul Hossain Date: Wed, 12 Aug 2026 10:07:18 +0600 Subject: [PATCH 2/3] test: cover the immutable Cache-Control field-lines --- test/send.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/send.js b/test/send.js index b824282..3378b57 100644 --- a/test/send.js +++ b/test/send.js @@ -1100,6 +1100,20 @@ describe('send(file, options)', function () { .get('/name.txt') .expect('Cache-Control', 'public, max-age=3600, immutable', done) }) + + it('should send the immutable directive as a separate field-line', function (done) { + request(createServer({ immutable: true, maxAge: '1h', root: fixtures })) + .get('/name.txt') + .expect(shouldHaveHeaderValues('Cache-Control', ['public, max-age=3600', 'immutable'])) + .expect(200, done) + }) + + it('should send a single field-line when disabled', function (done) { + request(createServer({ immutable: false, maxAge: '1h', root: fixtures })) + .get('/name.txt') + .expect(shouldHaveHeaderValues('Cache-Control', ['public, max-age=3600'])) + .expect(200, done) + }) }) describe('maxAge', function () { @@ -1317,6 +1331,22 @@ function createServer (opts, fn) { }) } +function shouldHaveHeaderValues (header, values) { + return function (res) { + var lower = header.toLowerCase() + var raw = res.res.rawHeaders + var actual = [] + + for (var i = 0; i < raw.length; i += 2) { + if (raw[i].toLowerCase() === lower) { + actual.push(raw[i + 1]) + } + } + + assert.deepStrictEqual(actual, values, 'should have ' + header + ' field-lines ' + JSON.stringify(values)) + } +} + function shouldNotHaveBody () { return function (res) { assert.ok(res.text === '' || res.text === undefined) From d5415a343cae7afc4cc7624eea765ffba2449b04 Mon Sep 17 00:00:00 2001 From: Nazmul Hossain Date: Wed, 12 Aug 2026 10:08:22 +0600 Subject: [PATCH 3/3] docs: document that immutable is sent as its own Cache-Control field-line --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 350fccd..b5ff74d 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,18 @@ also be specified to enable caching. The `immutable` directive will prevent supported clients from making conditional requests during the life of the `maxAge` option to check if the file has changed. +When enabled, the `immutable` directive is sent as its own `Cache-Control` +field-line instead of being appended to the `max-age` value, for example: + +``` +Cache-Control: public, max-age=31536000 +Cache-Control: immutable +``` + +HTTP caches combine repeated field-lines, so the effective set of directives is +unchanged. Splitting the value keeps the `public, max-age=` field-line +matchable against the HTTP/3 QPACK static table. + ##### index By default send supports "index.html" files, to disable this