From 60402ad4d5e6a1b75343cdcb103c885883f54ab1 Mon Sep 17 00:00:00 2001 From: KMS <243513302+AlyNotMe@users.noreply.github.com> Date: Wed, 23 Sep 2026 15:07:51 +0200 Subject: [PATCH] fix: add Last-Modified header to GET and HEAD responses Only sent when access is granted (200 path), using the resource's mtime from fs.stat, as required by the Solid protocol. Fixes #1793 --- lib/handlers/get.mjs | 9 ++++++++- lib/ldp.mjs | 6 +++--- test/integration/header-test.mjs | 17 +++++++++++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/lib/handlers/get.mjs b/lib/handlers/get.mjs index a76b42527..ef6c91d06 100644 --- a/lib/handlers/get.mjs +++ b/lib/handlers/get.mjs @@ -76,6 +76,7 @@ export default async function handler (req, res, next) { let container let contentRange let chunksize + let mtime if (ret) { stream = ret.stream @@ -83,12 +84,14 @@ export default async function handler (req, res, next) { container = ret.container contentRange = ret.contentRange chunksize = ret.chunksize + mtime = ret.mtime } // Till here it must exist if (!includeBody) { debug('HEAD only') res.setHeader('Content-Type', ret.contentType) + if (mtime) res.setHeader('Last-Modified', mtime.toUTCString()) return res.status(200).send('OK') } @@ -118,7 +121,8 @@ export default async function handler (req, res, next) { // If request accepts the content-type we found if (stream && negotiator.mediaType([contentType])) { let headers = { - 'Content-Type': contentType + 'Content-Type': contentType, + ...(mtime && { 'Last-Modified': mtime.toUTCString() }) } if (contentRange) { @@ -160,6 +164,9 @@ export default async function handler (req, res, next) { headers })) return res.setHeader('Content-Type', possibleRDFType) + if (mtime) { + res.setHeader('Last-Modified', mtime.toUTCString()) + } res.send(data) return next() } catch (err) { diff --git a/lib/ldp.mjs b/lib/ldp.mjs index 83dc25904..9ad27a263 100644 --- a/lib/ldp.mjs +++ b/lib/ldp.mjs @@ -451,7 +451,7 @@ class LDP { } if (!options.includeBody) { - return { stream: stats, contentType, container: stats.isDirectory() } + return { stream: stats, contentType, container: stats.isDirectory(), mtime: stats.mtime } } if (stats.isDirectory()) { @@ -465,7 +465,7 @@ class LDP { throw err } const stream = stringToStream(data) - return { stream, contentType, container: true } + return { stream, contentType, container: true, mtime: stats.mtime } } else { let chunksize, contentRange, start, end if (options.range) { @@ -487,7 +487,7 @@ class LDP { }) .on('open', function () { debug.handlers(`GET -- Reading ${pathLocal}`) - return resolve({ stream, contentType, container: false, contentRange, chunksize }) + return resolve({ stream, contentType, container: false, contentRange, chunksize, mtime: stats.mtime }) }) })) } diff --git a/test/integration/header-test.mjs b/test/integration/header-test.mjs index fe60ddd00..718d72490 100644 --- a/test/integration/header-test.mjs +++ b/test/integration/header-test.mjs @@ -68,6 +68,23 @@ describe('Header handler', () => { }) }) + describe('Last-Modified', () => { + describeHeaderTest('read/append for the public', { + resource: '/public-ra', + headers: { + 'Last-Modified': /^\w{3}, \d{2} \w{3} \d{4} \d{2}:\d{2}:\d{2} GMT$/ + } + }) + + describe('on a HEAD request', () => { + it('has a Last-Modified header', async () => { + const { headers } = await request.head('/public-ra') + expect(headers).to.have.property('last-modified') + expect(headers['last-modified']).to.match(/^\w{3}, \d{2} \w{3} \d{4} \d{2}:\d{2}:\d{2} GMT$/) + }) + }) + }) + function describeHeaderTest (label, { resource, headers }) { describe(`a resource that is ${label}`, () => { // Retrieve the response headers