Skip to content

Commit c235ca5

Browse files
committed
Add upstream max-age to optimized image
1 parent 6a5279a commit c235ca5

File tree

2 files changed

+100
-62
lines changed

2 files changed

+100
-62
lines changed

packages/next/next-server/server/image-optimizer.ts

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const PNG = 'image/png'
2222
const JPEG = 'image/jpeg'
2323
const GIF = 'image/gif'
2424
const SVG = 'image/svg+xml'
25-
const CACHE_VERSION = 2
25+
const CACHE_VERSION = 3
2626
const MODERN_TYPES = [/* AVIF, */ WEBP]
2727
const ANIMATABLE_TYPES = [WEBP, PNG, GIF]
2828
const VECTOR_TYPES = [SVG]
@@ -158,24 +158,23 @@ export async function imageOptimizer(
158158
if (await fileExists(hashDir, 'directory')) {
159159
const files = await promises.readdir(hashDir)
160160
for (let file of files) {
161-
const [prefix, etag, extension] = file.split('.')
162-
const expireAt = Number(prefix)
161+
const [maxAgeStr, expireAtSt, etag, extension] = file.split('.')
162+
const maxAge = Number(maxAgeStr)
163+
const expireAt = Number(expireAtSt)
163164
const contentType = getContentType(extension)
164165
const fsPath = join(hashDir, file)
165166
if (now < expireAt) {
166-
res.setHeader(
167-
'Cache-Control',
167+
const result = setResponseHeaders(
168+
req,
169+
res,
170+
etag,
171+
maxAge,
172+
contentType,
168173
isStatic
169-
? 'public, max-age=315360000, immutable'
170-
: 'public, max-age=0, must-revalidate'
171174
)
172-
if (sendEtagResponse(req, res, etag)) {
173-
return { finished: true }
175+
if (!result.finished) {
176+
createReadStream(fsPath).pipe(res)
174177
}
175-
if (contentType) {
176-
res.setHeader('Content-Type', contentType)
177-
}
178-
createReadStream(fsPath).pipe(res)
179178
return { finished: true }
180179
} else {
181180
await promises.unlink(fsPath)
@@ -271,8 +270,14 @@ export async function imageOptimizer(
271270
const animate =
272271
ANIMATABLE_TYPES.includes(upstreamType) && isAnimated(upstreamBuffer)
273272
if (vector || animate) {
274-
await writeToCacheDir(hashDir, upstreamType, expireAt, upstreamBuffer)
275-
sendResponse(req, res, upstreamType, upstreamBuffer, isStatic)
273+
await writeToCacheDir(
274+
hashDir,
275+
upstreamType,
276+
maxAge,
277+
expireAt,
278+
upstreamBuffer
279+
)
280+
sendResponse(req, res, maxAge, upstreamType, upstreamBuffer, isStatic)
276281
return { finished: true }
277282
}
278283

@@ -342,13 +347,19 @@ export async function imageOptimizer(
342347
}
343348

344349
if (optimizedBuffer) {
345-
await writeToCacheDir(hashDir, contentType, expireAt, optimizedBuffer)
346-
sendResponse(req, res, contentType, optimizedBuffer, isStatic)
350+
await writeToCacheDir(
351+
hashDir,
352+
contentType,
353+
maxAge,
354+
expireAt,
355+
optimizedBuffer
356+
)
357+
sendResponse(req, res, maxAge, contentType, optimizedBuffer, isStatic)
347358
} else {
348359
throw new Error('Unable to optimize buffer')
349360
}
350361
} catch (error) {
351-
sendResponse(req, res, upstreamType, upstreamBuffer, isStatic)
362+
sendResponse(req, res, maxAge, upstreamType, upstreamBuffer, isStatic)
352363
}
353364

354365
return { finished: true }
@@ -362,37 +373,61 @@ export async function imageOptimizer(
362373
async function writeToCacheDir(
363374
dir: string,
364375
contentType: string,
376+
maxAge: number,
365377
expireAt: number,
366378
buffer: Buffer
367379
) {
368380
await promises.mkdir(dir, { recursive: true })
369381
const extension = getExtension(contentType)
370382
const etag = getHash([buffer])
371-
const filename = join(dir, `${expireAt}.${etag}.${extension}`)
383+
const filename = join(dir, `${maxAge}.${expireAt}.${etag}.${extension}`)
372384
await promises.writeFile(filename, buffer)
373385
}
374386

375-
function sendResponse(
387+
function setResponseHeaders(
376388
req: IncomingMessage,
377389
res: ServerResponse,
390+
etag: string,
391+
maxAge: number,
378392
contentType: string | null,
379-
buffer: Buffer,
380393
isStatic: boolean
381394
) {
382-
const etag = getHash([buffer])
383395
res.setHeader(
384396
'Cache-Control',
385397
isStatic
386398
? 'public, max-age=315360000, immutable'
387-
: 'public, max-age=0, must-revalidate'
399+
: `public, max-age=${maxAge}, must-revalidate`
388400
)
389401
if (sendEtagResponse(req, res, etag)) {
390-
return
402+
// already called res.end() so we're finished
403+
return { finished: true }
391404
}
392405
if (contentType) {
393406
res.setHeader('Content-Type', contentType)
394407
}
395-
res.end(buffer)
408+
return { finished: false }
409+
}
410+
411+
function sendResponse(
412+
req: IncomingMessage,
413+
res: ServerResponse,
414+
maxAge: number,
415+
contentType: string | null,
416+
buffer: Buffer,
417+
isStatic: boolean
418+
) {
419+
const etag = getHash([buffer])
420+
const result = setResponseHeaders(
421+
req,
422+
res,
423+
etag,
424+
maxAge,
425+
contentType,
426+
isStatic
427+
)
428+
if (!result.finished) {
429+
res.end(buffer)
430+
}
396431
}
397432

398433
function getSupportedMimeType(options: string[], accept = ''): string {

test/integration/image-optimizer/test/index.test.js

Lines changed: 40 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ function runTests({ w, isDev, domains }) {
5656
const res = await fetchViaHTTP(appPort, '/_next/image', query, {})
5757
expect(res.status).toBe(200)
5858
expect(res.headers.get('content-type')).toContain('image/gif')
59-
expect(res.headers.get('cache-control')).toBe(
60-
'public, max-age=0, must-revalidate'
59+
expect(res.headers.get('Cache-Control')).toBe(
60+
'public, max-age=60, must-revalidate'
6161
)
6262
expect(res.headers.get('etag')).toBeTruthy()
6363
expect(isAnimated(await res.buffer())).toBe(true)
@@ -68,8 +68,8 @@ function runTests({ w, isDev, domains }) {
6868
const res = await fetchViaHTTP(appPort, '/_next/image', query, {})
6969
expect(res.status).toBe(200)
7070
expect(res.headers.get('content-type')).toContain('image/png')
71-
expect(res.headers.get('cache-control')).toBe(
72-
'public, max-age=0, must-revalidate'
71+
expect(res.headers.get('Cache-Control')).toBe(
72+
'public, max-age=60, must-revalidate'
7373
)
7474
expect(res.headers.get('etag')).toBeTruthy()
7575
expect(isAnimated(await res.buffer())).toBe(true)
@@ -80,8 +80,8 @@ function runTests({ w, isDev, domains }) {
8080
const res = await fetchViaHTTP(appPort, '/_next/image', query, {})
8181
expect(res.status).toBe(200)
8282
expect(res.headers.get('content-type')).toContain('image/webp')
83-
expect(res.headers.get('cache-control')).toBe(
84-
'public, max-age=0, must-revalidate'
83+
expect(res.headers.get('Cache-Control')).toBe(
84+
'public, max-age=60, must-revalidate'
8585
)
8686
expect(res.headers.get('etag')).toBeTruthy()
8787
expect(isAnimated(await res.buffer())).toBe(true)
@@ -93,8 +93,8 @@ function runTests({ w, isDev, domains }) {
9393
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
9494
expect(res.status).toBe(200)
9595
expect(res.headers.get('Content-Type')).toContain('image/svg+xml')
96-
expect(res.headers.get('cache-control')).toBe(
97-
'public, max-age=0, must-revalidate'
96+
expect(res.headers.get('Cache-Control')).toBe(
97+
'public, max-age=60, must-revalidate'
9898
)
9999
expect(res.headers.get('etag')).toBeTruthy()
100100
const actual = await res.text()
@@ -111,8 +111,8 @@ function runTests({ w, isDev, domains }) {
111111
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
112112
expect(res.status).toBe(200)
113113
expect(res.headers.get('Content-Type')).toContain('image/x-icon')
114-
expect(res.headers.get('cache-control')).toBe(
115-
'public, max-age=0, must-revalidate'
114+
expect(res.headers.get('Cache-Control')).toBe(
115+
'public, max-age=60, must-revalidate'
116116
)
117117
expect(res.headers.get('etag')).toBeTruthy()
118118
const actual = await res.text()
@@ -131,8 +131,8 @@ function runTests({ w, isDev, domains }) {
131131
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
132132
expect(res.status).toBe(200)
133133
expect(res.headers.get('Content-Type')).toContain('image/jpeg')
134-
expect(res.headers.get('cache-control')).toBe(
135-
'public, max-age=0, must-revalidate'
134+
expect(res.headers.get('Cache-Control')).toBe(
135+
'public, max-age=60, must-revalidate'
136136
)
137137
expect(res.headers.get('etag')).toBeTruthy()
138138
})
@@ -145,8 +145,8 @@ function runTests({ w, isDev, domains }) {
145145
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
146146
expect(res.status).toBe(200)
147147
expect(res.headers.get('Content-Type')).toContain('image/png')
148-
expect(res.headers.get('cache-control')).toBe(
149-
'public, max-age=0, must-revalidate'
148+
expect(res.headers.get('Cache-Control')).toBe(
149+
'public, max-age=60, must-revalidate'
150150
)
151151
expect(res.headers.get('etag')).toBeTruthy()
152152
})
@@ -242,8 +242,8 @@ function runTests({ w, isDev, domains }) {
242242
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
243243
expect(res.status).toBe(200)
244244
expect(res.headers.get('Content-Type')).toBe('image/webp')
245-
expect(res.headers.get('cache-control')).toBe(
246-
'public, max-age=0, must-revalidate'
245+
expect(res.headers.get('Cache-Control')).toBe(
246+
'public, max-age=60, must-revalidate'
247247
)
248248
expect(res.headers.get('etag')).toBeTruthy()
249249
await expectWidth(res, w)
@@ -255,8 +255,8 @@ function runTests({ w, isDev, domains }) {
255255
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
256256
expect(res.status).toBe(200)
257257
expect(res.headers.get('Content-Type')).toBe('image/png')
258-
expect(res.headers.get('cache-control')).toBe(
259-
'public, max-age=0, must-revalidate'
258+
expect(res.headers.get('Cache-Control')).toBe(
259+
'public, max-age=60, must-revalidate'
260260
)
261261
expect(res.headers.get('etag')).toBeTruthy()
262262
await expectWidth(res, w)
@@ -268,8 +268,8 @@ function runTests({ w, isDev, domains }) {
268268
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
269269
expect(res.status).toBe(200)
270270
expect(res.headers.get('Content-Type')).toBe('image/png')
271-
expect(res.headers.get('cache-control')).toBe(
272-
'public, max-age=0, must-revalidate'
271+
expect(res.headers.get('Cache-Control')).toBe(
272+
'public, max-age=60, must-revalidate'
273273
)
274274
expect(res.headers.get('etag')).toBeTruthy()
275275
await expectWidth(res, w)
@@ -281,8 +281,8 @@ function runTests({ w, isDev, domains }) {
281281
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
282282
expect(res.status).toBe(200)
283283
expect(res.headers.get('Content-Type')).toBe('image/gif')
284-
expect(res.headers.get('cache-control')).toBe(
285-
'public, max-age=0, must-revalidate'
284+
expect(res.headers.get('Cache-Control')).toBe(
285+
'public, max-age=60, must-revalidate'
286286
)
287287
expect(res.headers.get('etag')).toBeTruthy()
288288
// FIXME: await expectWidth(res, w)
@@ -294,8 +294,8 @@ function runTests({ w, isDev, domains }) {
294294
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
295295
expect(res.status).toBe(200)
296296
expect(res.headers.get('Content-Type')).toBe('image/tiff')
297-
expect(res.headers.get('cache-control')).toBe(
298-
'public, max-age=0, must-revalidate'
297+
expect(res.headers.get('Cache-Control')).toBe(
298+
'public, max-age=60, must-revalidate'
299299
)
300300
expect(res.headers.get('etag')).toBeTruthy()
301301
// FIXME: await expectWidth(res, w)
@@ -309,8 +309,8 @@ function runTests({ w, isDev, domains }) {
309309
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
310310
expect(res.status).toBe(200)
311311
expect(res.headers.get('Content-Type')).toBe('image/webp')
312-
expect(res.headers.get('cache-control')).toBe(
313-
'public, max-age=0, must-revalidate'
312+
expect(res.headers.get('Cache-Control')).toBe(
313+
'public, max-age=60, must-revalidate'
314314
)
315315
expect(res.headers.get('etag')).toBeTruthy()
316316
await expectWidth(res, w)
@@ -324,8 +324,8 @@ function runTests({ w, isDev, domains }) {
324324
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
325325
expect(res.status).toBe(200)
326326
expect(res.headers.get('Content-Type')).toBe('image/webp')
327-
expect(res.headers.get('cache-control')).toBe(
328-
'public, max-age=0, must-revalidate'
327+
expect(res.headers.get('Cache-Control')).toBe(
328+
'public, max-age=60, must-revalidate'
329329
)
330330
expect(res.headers.get('etag')).toBeTruthy()
331331
await expectWidth(res, w)
@@ -427,7 +427,7 @@ function runTests({ w, isDev, domains }) {
427427
expect(res1.status).toBe(200)
428428
expect(res1.headers.get('Content-Type')).toBe('image/webp')
429429
expect(res1.headers.get('Cache-Control')).toBe(
430-
'public, max-age=0, must-revalidate'
430+
'public, max-age=60, must-revalidate'
431431
)
432432
const etag = res1.headers.get('Etag')
433433
expect(etag).toBeTruthy()
@@ -438,14 +438,17 @@ function runTests({ w, isDev, domains }) {
438438
expect(res2.status).toBe(304)
439439
expect(res2.headers.get('Content-Type')).toBeFalsy()
440440
expect(res2.headers.get('Etag')).toBe(etag)
441+
expect(res2.headers.get('Cache-Control')).toBe(
442+
'public, max-age=60, must-revalidate'
443+
)
441444
expect((await res2.buffer()).length).toBe(0)
442445

443446
const query3 = { url: '/test.jpg', w, q: 25 }
444447
const res3 = await fetchViaHTTP(appPort, '/_next/image', query3, opts2)
445448
expect(res3.status).toBe(200)
446449
expect(res3.headers.get('Content-Type')).toBe('image/webp')
447450
expect(res3.headers.get('Cache-Control')).toBe(
448-
'public, max-age=0, must-revalidate'
451+
'public, max-age=60, must-revalidate'
449452
)
450453
expect(res3.headers.get('Etag')).toBeTruthy()
451454
expect(res3.headers.get('Etag')).not.toBe(etag)
@@ -461,8 +464,8 @@ function runTests({ w, isDev, domains }) {
461464
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
462465
expect(res.status).toBe(200)
463466
expect(res.headers.get('Content-Type')).toBe('image/bmp')
464-
expect(res.headers.get('cache-control')).toBe(
465-
'public, max-age=0, must-revalidate'
467+
expect(res.headers.get('Cache-Control')).toBe(
468+
'public, max-age=60, must-revalidate'
466469
)
467470
expect(res.headers.get('etag')).toBeTruthy()
468471

@@ -476,8 +479,8 @@ function runTests({ w, isDev, domains }) {
476479
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
477480
expect(res.status).toBe(200)
478481
expect(res.headers.get('Content-Type')).toBe('image/webp')
479-
expect(res.headers.get('cache-control')).toBe(
480-
'public, max-age=0, must-revalidate'
482+
expect(res.headers.get('Cache-Control')).toBe(
483+
'public, max-age=60, must-revalidate'
481484
)
482485
expect(res.headers.get('etag')).toBeTruthy()
483486
await expectWidth(res, 400)
@@ -491,8 +494,8 @@ function runTests({ w, isDev, domains }) {
491494
const res = await fetchViaHTTP(appPort, '/_next/image', query, opts)
492495
expect(res.status).toBe(200)
493496
expect(res.headers.get('Content-Type')).toBe('image/png')
494-
expect(res.headers.get('cache-control')).toBe(
495-
'public, max-age=0, must-revalidate'
497+
expect(res.headers.get('Cache-Control')).toBe(
498+
'public, max-age=60, must-revalidate'
496499
)
497500

498501
const png = await res.buffer()
@@ -553,7 +556,7 @@ function runTests({ w, isDev, domains }) {
553556
await expectWidth(res2, w)
554557

555558
// There should be only one image created in the cache directory.
556-
const hashItems = [2, '/test.png', w, 80, 'image/webp']
559+
const hashItems = [3, '/test.png', w, 80, 'image/webp']
557560
const hash = createHash('sha256')
558561
for (let item of hashItems) {
559562
if (typeof item === 'number') hash.update(String(item))

0 commit comments

Comments
 (0)