Skip to content

Commit d670198

Browse files
authored
Add "Vary: Accept" header to /_next/image responses (#26788)
This pull request adds "Vary: Accept" header to responses from the image optimizer (i.e. the /_next/image endpoint). The image optimizer prefers re-encoding JPG files to WebP, but some browsers (such as Safari 14 on Catalina) do not yet support WebP. In such cases the optimizer uses the Accept header sent by the browser to send out a JPG response. Thus the optimizer's response may depend on the Accept header. Potential caching proxies can be informed of this fact by adding "Vary: Accept" to the response headers. Otherwise WebP data may be served to browsers that do not support it, for example in the following scenario: * A browser that supports WebP requests the JPG. The optimizer re-encodes it to WebP. The proxy caches the WebP data. * After this another browser that doesn't support WebP requests the JPG. The proxy sends the WebP data to the browser. - [x] Integration tests added - [x] Make sure the linting passes
1 parent 93f6254 commit d670198

File tree

2 files changed

+29
-0
lines changed

2 files changed

+29
-0
lines changed

packages/next/server/image-optimizer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,7 @@ function setResponseHeaders(
419419
isStatic: boolean,
420420
isDev: boolean
421421
) {
422+
res.setHeader('Vary', 'Accept')
422423
res.setHeader(
423424
'Cache-Control',
424425
isStatic

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ function runTests({ w, isDev, domains }) {
5858
expect(res.headers.get('Cache-Control')).toBe(
5959
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
6060
)
61+
expect(res.headers.get('Vary')).toBe('Accept')
6162
expect(res.headers.get('etag')).toBeTruthy()
6263
expect(isAnimated(await res.buffer())).toBe(true)
6364
})
@@ -70,6 +71,7 @@ function runTests({ w, isDev, domains }) {
7071
expect(res.headers.get('Cache-Control')).toBe(
7172
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
7273
)
74+
expect(res.headers.get('Vary')).toBe('Accept')
7375
expect(res.headers.get('etag')).toBeTruthy()
7476
expect(isAnimated(await res.buffer())).toBe(true)
7577
})
@@ -82,6 +84,7 @@ function runTests({ w, isDev, domains }) {
8284
expect(res.headers.get('Cache-Control')).toBe(
8385
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
8486
)
87+
expect(res.headers.get('Vary')).toBe('Accept')
8588
expect(res.headers.get('etag')).toBeTruthy()
8689
expect(isAnimated(await res.buffer())).toBe(true)
8790
})
@@ -95,6 +98,9 @@ function runTests({ w, isDev, domains }) {
9598
expect(res.headers.get('Cache-Control')).toBe(
9699
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
97100
)
101+
// SVG is compressible so will have accept-encoding set from
102+
// compression
103+
expect(res.headers.get('Vary')).toMatch(/^Accept(,|$)/)
98104
expect(res.headers.get('etag')).toBeTruthy()
99105
const actual = await res.text()
100106
const expected = await fs.readFile(
@@ -113,6 +119,7 @@ function runTests({ w, isDev, domains }) {
113119
expect(res.headers.get('Cache-Control')).toBe(
114120
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
115121
)
122+
expect(res.headers.get('Vary')).toMatch(/^Accept(,|$)/)
116123
expect(res.headers.get('etag')).toBeTruthy()
117124
const actual = await res.text()
118125
const expected = await fs.readFile(
@@ -133,6 +140,7 @@ function runTests({ w, isDev, domains }) {
133140
expect(res.headers.get('Cache-Control')).toBe(
134141
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
135142
)
143+
expect(res.headers.get('Vary')).toBe('Accept')
136144
expect(res.headers.get('etag')).toBeTruthy()
137145
})
138146

@@ -147,6 +155,7 @@ function runTests({ w, isDev, domains }) {
147155
expect(res.headers.get('Cache-Control')).toBe(
148156
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
149157
)
158+
expect(res.headers.get('Vary')).toBe('Accept')
150159
expect(res.headers.get('etag')).toBeTruthy()
151160
})
152161

@@ -244,6 +253,7 @@ function runTests({ w, isDev, domains }) {
244253
expect(res.headers.get('Cache-Control')).toBe(
245254
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
246255
)
256+
expect(res.headers.get('Vary')).toBe('Accept')
247257
expect(res.headers.get('etag')).toBeTruthy()
248258
await expectWidth(res, w)
249259
})
@@ -257,6 +267,7 @@ function runTests({ w, isDev, domains }) {
257267
expect(res.headers.get('Cache-Control')).toBe(
258268
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
259269
)
270+
expect(res.headers.get('Vary')).toBe('Accept')
260271
expect(res.headers.get('etag')).toBeTruthy()
261272
await expectWidth(res, w)
262273
})
@@ -270,6 +281,7 @@ function runTests({ w, isDev, domains }) {
270281
expect(res.headers.get('Cache-Control')).toBe(
271282
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
272283
)
284+
expect(res.headers.get('Vary')).toBe('Accept')
273285
expect(res.headers.get('etag')).toBeTruthy()
274286
await expectWidth(res, w)
275287
})
@@ -283,6 +295,7 @@ function runTests({ w, isDev, domains }) {
283295
expect(res.headers.get('Cache-Control')).toBe(
284296
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
285297
)
298+
expect(res.headers.get('Vary')).toBe('Accept')
286299
expect(res.headers.get('etag')).toBeTruthy()
287300
// FIXME: await expectWidth(res, w)
288301
})
@@ -296,6 +309,7 @@ function runTests({ w, isDev, domains }) {
296309
expect(res.headers.get('Cache-Control')).toBe(
297310
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
298311
)
312+
expect(res.headers.get('Vary')).toBe('Accept')
299313
expect(res.headers.get('etag')).toBeTruthy()
300314
// FIXME: await expectWidth(res, w)
301315
})
@@ -311,6 +325,7 @@ function runTests({ w, isDev, domains }) {
311325
expect(res.headers.get('Cache-Control')).toBe(
312326
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
313327
)
328+
expect(res.headers.get('Vary')).toBe('Accept')
314329
expect(res.headers.get('etag')).toBeTruthy()
315330
await expectWidth(res, w)
316331
})
@@ -326,6 +341,7 @@ function runTests({ w, isDev, domains }) {
326341
expect(res.headers.get('Cache-Control')).toBe(
327342
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
328343
)
344+
expect(res.headers.get('Vary')).toBe('Accept')
329345
expect(res.headers.get('etag')).toBeTruthy()
330346
await expectWidth(res, w)
331347
})
@@ -346,6 +362,7 @@ function runTests({ w, isDev, domains }) {
346362
expect(res.headers.get('Cache-Control')).toBe(
347363
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
348364
)
365+
expect(res.headers.get('Vary')).toBe('Accept')
349366
expect(res.headers.get('etag')).toBeTruthy()
350367
await expectWidth(res, w)
351368
})
@@ -448,6 +465,7 @@ function runTests({ w, isDev, domains }) {
448465
expect(res1.headers.get('Cache-Control')).toBe(
449466
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
450467
)
468+
expect(res1.headers.get('Vary')).toBe('Accept')
451469
const etag = res1.headers.get('Etag')
452470
expect(etag).toBeTruthy()
453471
await expectWidth(res1, w)
@@ -460,6 +478,7 @@ function runTests({ w, isDev, domains }) {
460478
expect(res2.headers.get('Cache-Control')).toBe(
461479
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
462480
)
481+
expect(res2.headers.get('Vary')).toBe('Accept')
463482
expect((await res2.buffer()).length).toBe(0)
464483

465484
const query3 = { url: '/test.jpg', w, q: 25 }
@@ -469,6 +488,7 @@ function runTests({ w, isDev, domains }) {
469488
expect(res3.headers.get('Cache-Control')).toBe(
470489
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
471490
)
491+
expect(res3.headers.get('Vary')).toBe('Accept')
472492
expect(res3.headers.get('Etag')).toBeTruthy()
473493
expect(res3.headers.get('Etag')).not.toBe(etag)
474494
await expectWidth(res3, w)
@@ -486,6 +506,9 @@ function runTests({ w, isDev, domains }) {
486506
expect(res.headers.get('Cache-Control')).toBe(
487507
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
488508
)
509+
// bmp is compressible so will have accept-encoding set from
510+
// compression
511+
expect(res.headers.get('Vary')).toMatch(/^Accept(,|$)/)
489512
expect(res.headers.get('etag')).toBeTruthy()
490513

491514
const json2 = await fsToJson(imagesDir)
@@ -501,6 +524,7 @@ function runTests({ w, isDev, domains }) {
501524
expect(res.headers.get('Cache-Control')).toBe(
502525
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
503526
)
527+
expect(res.headers.get('Vary')).toBe('Accept')
504528
expect(res.headers.get('etag')).toBeTruthy()
505529
await expectWidth(res, 400)
506530
})
@@ -516,6 +540,7 @@ function runTests({ w, isDev, domains }) {
516540
expect(res.headers.get('Cache-Control')).toBe(
517541
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
518542
)
543+
expect(res.headers.get('Vary')).toBe('Accept')
519544

520545
const png = await res.buffer()
521546

@@ -540,6 +565,7 @@ function runTests({ w, isDev, domains }) {
540565
expect(res1.headers.get('Cache-Control')).toBe(
541566
'public, max-age=315360000, immutable'
542567
)
568+
expect(res1.headers.get('Vary')).toBe('Accept')
543569
await expectWidth(res1, w)
544570

545571
// Ensure subsequent request also has immutable header
@@ -548,6 +574,7 @@ function runTests({ w, isDev, domains }) {
548574
expect(res2.headers.get('Cache-Control')).toBe(
549575
'public, max-age=315360000, immutable'
550576
)
577+
expect(res2.headers.get('Vary')).toBe('Accept')
551578
await expectWidth(res2, w)
552579
}
553580
})
@@ -873,6 +900,7 @@ describe('Image Optimizer', () => {
873900
expect(res.headers.get('Cache-Control')).toBe(
874901
`public, max-age=31536000, must-revalidate`
875902
)
903+
expect(res.headers.get('Vary')).toBe('Accept')
876904
await expectWidth(res, 64)
877905
})
878906
})

0 commit comments

Comments
 (0)