diff --git a/packages/backend/src/server/file/FileServerFileResolver.ts b/packages/backend/src/server/file/FileServerFileResolver.ts index 687d486efd..254e97af65 100644 --- a/packages/backend/src/server/file/FileServerFileResolver.ts +++ b/packages/backend/src/server/file/FileServerFileResolver.ts @@ -112,6 +112,14 @@ export class FileServerFileResolver { }; } + // Verify the file actually exists on disk before returning. + // Unlike the thumbnail/webpublic cases above where detectType() implicitly + // checks this, the original file path is returned without an existence check. + // Without this check, the handler would set immutable cache headers before + // discovering the file is missing, causing CDN caches to cache the error + // response for 1 year (max-age=31536000, immutable) instead of max-age=300. + await fs.promises.access(path); + return { kind: 'stored', fileRole: 'original', diff --git a/packages/backend/test/unit/server/FileServerService.ts b/packages/backend/test/unit/server/FileServerService.ts index c88175c5c7..da54512346 100644 --- a/packages/backend/test/unit/server/FileServerService.ts +++ b/packages/backend/test/unit/server/FileServerService.ts @@ -479,6 +479,24 @@ describe('FileServerService', () => { expect(res.headers['cache-control']).toBe('max-age=86400'); }); + test('GET /files/:key DBに存在するがストレージ上に存在しないファイルにアクセスしたときエラーのキャッシュをimmutableにしない', async () => { + const accessKey = randomString(); + // Insert into DB as storedInternal but do NOT write the file to disk + await insertDriveFile({ + accessKey, + storedInternal: true, + isLink: false, + }); + + const res = await fastify.inject({ + method: 'GET', + url: `/files/${accessKey}`, + }); + + expect(res.statusCode).toBe(500); + expect(res.headers['cache-control']).toBe('max-age=300'); + }); + test('GET /files/:key 外部リンクを取得して配信する', async () => { const accessKey = randomString(); await insertDriveFile({ @@ -766,5 +784,26 @@ describe('FileServerService', () => { expect(res.headers['cache-control']).toBe('max-age=31536000, immutable'); expect(res.headers['content-disposition'] ?? '').toContain('dummy.png'); }); + + test('GET /proxy/:url* DBに存在するがストレージ上に存在しないファイルにアクセスしたときエラーのキャッシュをimmutableにしない', async () => { + const accessKey = randomString(); + // Insert into DB as storedInternal but do NOT write the file to disk + await insertDriveFile({ + accessKey, + storedInternal: true, + isLink: false, + }); + + const res = await fastify.inject({ + method: 'GET', + url: `/proxy/any?url=${encodeURIComponent(`${config.url}/files/${accessKey}`)}&origin=1`, + headers: { + 'user-agent': 'Mozilla/5.0', + }, + }); + + expect(res.statusCode).toBe(500); + expect(res.headers['cache-control']).toBe('max-age=300'); + }); }); });