diff --git a/src/storage/backend/file.test.ts b/src/storage/backend/file.test.ts index d44ab1dfc..50cbbce73 100644 --- a/src/storage/backend/file.test.ts +++ b/src/storage/backend/file.test.ts @@ -920,6 +920,19 @@ describe('FileBackend conditional reads', () => { await expect(statusFor({ ifNoneMatch: head.eTag })).resolves.toBe(304) }) + it.each([ + ['a weak tag', (eTag: string) => `W/${eTag}`], + ['a tag list', (eTag: string) => `"stale-etag", ${eTag}`], + ['a wildcard', () => '*'], + ])('returns 304 when if-none-match is %s matching the etag', async (_name, toHeader) => { + const head = await backend.headObject(bucket, key, version) + await expect(statusFor({ ifNoneMatch: toHeader(head.eTag) })).resolves.toBe(304) + }) + + it('returns 200 when no tag in an if-none-match list matches', async () => { + await expect(statusFor({ ifNoneMatch: '"stale-etag", W/"other-etag"' })).resolves.toBe(200) + }) + it('ignores if-modified-since when if-none-match is present and does not match', async () => { await expect( statusFor({ ifNoneMatch: '"stale-etag"', ifModifiedSince: lastModifiedHeader }) diff --git a/src/storage/backend/file.ts b/src/storage/backend/file.ts index f6cd9fb05..3b60085a8 100644 --- a/src/storage/backend/file.ts +++ b/src/storage/backend/file.ts @@ -48,6 +48,15 @@ const METADATA_ATTR_KEYS = { }, } +// RFC 9110 13.1.2: "*" matches any current representation; +// otherwise any listed entity-tag matches under weak comparison. +function ifNoneMatchMatches(ifNoneMatch: string, eTag: string): boolean { + if (ifNoneMatch.trim() === '*') { + return true + } + return ifNoneMatch.split(',').some((tag) => tag.trim().replace(/^W\//, '') === eTag) +} + /** * FileBackend * Interacts with the file system with this FileBackend adapter @@ -101,7 +110,7 @@ export class FileBackend implements StorageBackendAdapter { const { cacheControl, contentType } = await this.getFileMetadata(file) const lastModified = data.mtime - if (headers?.ifNoneMatch && headers.ifNoneMatch === eTag) { + if (headers?.ifNoneMatch && ifNoneMatchMatches(headers.ifNoneMatch, eTag)) { return { metadata: { cacheControl: cacheControl || 'no-cache',