From d8b1878a754eeea3b303ba8c221f6d14210c267b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 09:23:16 +0700 Subject: [PATCH] fix: add ETag and headers to 304 Not Modified responses AWS SDK requires ETag header in 304 responses. Without it, the SDK throws UnknownError despite receiving a valid 304 status code. Co-Authored-By: Claude Opus 5 (1M context) --- .../http/controllers/s3-controller.ts | 60 +++++++++++++------ src/utils/s3/auth.ts | 2 +- 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/src/interfaces/http/controllers/s3-controller.ts b/src/interfaces/http/controllers/s3-controller.ts index b0aca88..ce2d2be 100644 --- a/src/interfaces/http/controllers/s3-controller.ts +++ b/src/interfaces/http/controllers/s3-controller.ts @@ -477,6 +477,7 @@ const handleGetObject = async ( // H3: Conditional headers — If-Match / If-None-Match const etag = `"${file.fileHash || nanoid(16)}"`; + const lastModified = file.createdAt instanceof Date ? file.createdAt : new Date(file.createdAt); const ifMatch = headers['if-match']; if (ifMatch && ifMatch !== '*' && ifMatch !== etag) { return s3ErrorResponse( @@ -489,22 +490,33 @@ const handleGetObject = async ( } const ifNoneMatch = headers['if-none-match']; if (ifNoneMatch && ifNoneMatch === etag) { - return new Response(null, { status: 304, headers: s3Headers(reqId, { - etag, 'content-type': file.mimeType, 'content-length': String(file.sizeBytes), - 'last-modified': lastModified.toUTCString(), 'x-amz-version-id': 'null', - }) }); + return new Response(null, { + status: 304, + headers: s3Headers(reqId, { + etag, + 'content-type': file.mimeType, + 'content-length': String(file.sizeBytes), + 'last-modified': lastModified.toUTCString(), + 'x-amz-version-id': 'null', + }), + }); } // H3: Conditional headers — If-Modified-Since / If-Unmodified-Since - const lastModified = file.createdAt instanceof Date ? file.createdAt : new Date(file.createdAt); const ifModifiedSince = headers['if-modified-since']; if (ifModifiedSince) { const since = new Date(ifModifiedSince); if (!Number.isNaN(since.getTime()) && lastModified.getTime() <= since.getTime()) { - return new Response(null, { status: 304, headers: s3Headers(reqId, { - etag, 'content-type': file.mimeType, 'content-length': String(file.sizeBytes), - 'last-modified': lastModified.toUTCString(), 'x-amz-version-id': 'null', - }) }); + return new Response(null, { + status: 304, + headers: s3Headers(reqId, { + etag, + 'content-type': file.mimeType, + 'content-length': String(file.sizeBytes), + 'last-modified': lastModified.toUTCString(), + 'x-amz-version-id': 'null', + }), + }); } } const ifUnmodifiedSince = headers['if-unmodified-since']; @@ -733,6 +745,7 @@ const handleHeadObject = async ( // H3: Conditional headers for HEAD — If-Match / If-None-Match const etag = `"${file.fileHash || nanoid(16)}"`; + const lastModified = file.createdAt instanceof Date ? file.createdAt : new Date(file.createdAt); const ifMatch = headers['if-match']; if (ifMatch && ifMatch !== '*' && ifMatch !== etag) { return s3ErrorResponse( @@ -745,22 +758,33 @@ const handleHeadObject = async ( } const ifNoneMatch = headers['if-none-match']; if (ifNoneMatch && ifNoneMatch === etag) { - return new Response(null, { status: 304, headers: s3Headers(reqId, { - etag, 'content-type': file.mimeType, 'content-length': String(file.sizeBytes), - 'last-modified': lastModified.toUTCString(), 'x-amz-version-id': 'null', - }) }); + return new Response(null, { + status: 304, + headers: s3Headers(reqId, { + etag, + 'content-type': file.mimeType, + 'content-length': String(file.sizeBytes), + 'last-modified': lastModified.toUTCString(), + 'x-amz-version-id': 'null', + }), + }); } // H3: Conditional headers for HEAD — If-Modified-Since / If-Unmodified-Since - const lastModified = file.createdAt instanceof Date ? file.createdAt : new Date(file.createdAt); const ifModifiedSince = headers['if-modified-since']; if (ifModifiedSince) { const since = new Date(ifModifiedSince); if (!Number.isNaN(since.getTime()) && lastModified.getTime() <= since.getTime()) { - return new Response(null, { status: 304, headers: s3Headers(reqId, { - etag, 'content-type': file.mimeType, 'content-length': String(file.sizeBytes), - 'last-modified': lastModified.toUTCString(), 'x-amz-version-id': 'null', - }) }); + return new Response(null, { + status: 304, + headers: s3Headers(reqId, { + etag, + 'content-type': file.mimeType, + 'content-length': String(file.sizeBytes), + 'last-modified': lastModified.toUTCString(), + 'x-amz-version-id': 'null', + }), + }); } } const ifUnmodifiedSince = headers['if-unmodified-since']; diff --git a/src/utils/s3/auth.ts b/src/utils/s3/auth.ts index e1fee52..720b3ed 100644 --- a/src/utils/s3/auth.ts +++ b/src/utils/s3/auth.ts @@ -297,7 +297,7 @@ export const verifySignature = async ( const hashedCanonicalRequest = await sha256Hex(canonicalRequest); // M1: Fall back to Date header if x-amz-date is missing - const amzDate = headers['x-amz-date'] || headers['date'] || ''; + const amzDate = headers['x-amz-date'] || headers.date || ''; // H5: Validate request freshness (clock skew / replay protection) if (amzDate) {