diff --git a/packages/next/next-server/server/image-optimizer.ts b/packages/next/next-server/server/image-optimizer.ts index 9c19ee031d24707..ff7037f241b4921 100644 --- a/packages/next/next-server/server/image-optimizer.ts +++ b/packages/next/next-server/server/image-optimizer.ts @@ -218,17 +218,19 @@ export async function imageOptimizer( } } + const expireAt = maxAge * 1000 + now + if (upstreamType) { const vector = VECTOR_TYPES.includes(upstreamType) const animate = ANIMATABLE_TYPES.includes(upstreamType) && isAnimated(upstreamBuffer) if (vector || animate) { + await writeToCacheDir(hashDir, upstreamType, expireAt, upstreamBuffer) sendResponse(req, res, upstreamType, upstreamBuffer) return { finished: true } } } - const expireAt = maxAge * 1000 + now let contentType: string if (mimeType) { @@ -276,11 +278,7 @@ export async function imageOptimizer( } const optimizedBuffer = await transformer.toBuffer() - await promises.mkdir(hashDir, { recursive: true }) - const extension = getExtension(contentType) - const etag = getHash([optimizedBuffer]) - const filename = join(hashDir, `${expireAt}.${etag}.${extension}`) - await promises.writeFile(filename, optimizedBuffer) + await writeToCacheDir(hashDir, contentType, expireAt, optimizedBuffer) sendResponse(req, res, contentType, optimizedBuffer) } catch (error) { sendResponse(req, res, upstreamType, upstreamBuffer) @@ -289,6 +287,19 @@ export async function imageOptimizer( return { finished: true } } +async function writeToCacheDir( + dir: string, + contentType: string, + expireAt: number, + buffer: Buffer +) { + await promises.mkdir(dir, { recursive: true }) + const extension = getExtension(contentType) + const etag = getHash([buffer]) + const filename = join(dir, `${expireAt}.${etag}.${extension}`) + await promises.writeFile(filename, buffer) +} + function sendResponse( req: IncomingMessage, res: ServerResponse, diff --git a/test/integration/image-optimizer/test/index.test.js b/test/integration/image-optimizer/test/index.test.js index 2b095c2d0225864..0e73ecb56446cee 100644 --- a/test/integration/image-optimizer/test/index.test.js +++ b/test/integration/image-optimizer/test/index.test.js @@ -380,6 +380,44 @@ function runTests({ w, isDev, domains }) { expect(json2).toStrictEqual(json1) }) + it('should use cached image file when parameters are the same for svg', async () => { + await fs.remove(imagesDir) + + const query = { url: '/test.svg', w, q: 80 } + const opts = { headers: { accept: 'image/webp' } } + + const res1 = await fetchViaHTTP(appPort, '/_next/image', query, opts) + expect(res1.status).toBe(200) + expect(res1.headers.get('Content-Type')).toBe('image/svg+xml') + const json1 = await fsToJson(imagesDir) + expect(Object.keys(json1).length).toBe(1) + + const res2 = await fetchViaHTTP(appPort, '/_next/image', query, opts) + expect(res2.status).toBe(200) + expect(res2.headers.get('Content-Type')).toBe('image/svg+xml') + const json2 = await fsToJson(imagesDir) + expect(json2).toStrictEqual(json1) + }) + + it('should use cached image file when parameters are the same for animated gif', async () => { + await fs.remove(imagesDir) + + const query = { url: '/animated.gif', w, q: 80 } + const opts = { headers: { accept: 'image/webp' } } + + const res1 = await fetchViaHTTP(appPort, '/_next/image', query, opts) + expect(res1.status).toBe(200) + expect(res1.headers.get('Content-Type')).toBe('image/gif') + const json1 = await fsToJson(imagesDir) + expect(Object.keys(json1).length).toBe(1) + + const res2 = await fetchViaHTTP(appPort, '/_next/image', query, opts) + expect(res2.status).toBe(200) + expect(res2.headers.get('Content-Type')).toBe('image/gif') + const json2 = await fsToJson(imagesDir) + expect(json2).toStrictEqual(json1) + }) + it('should set 304 status without body when etag matches if-none-match', async () => { const query = { url: '/test.jpg', w, q: 80 } const opts1 = { headers: { accept: 'image/webp' } }