Skip to content

Commit

Permalink
Add "Vary: Accept" header to /_next/image responses (#26788)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
jviide committed Jul 1, 2021
1 parent 93f6254 commit d670198
Show file tree
Hide file tree
Showing 2 changed files with 29 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/next/server/image-optimizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ function setResponseHeaders(
isStatic: boolean,
isDev: boolean
) {
res.setHeader('Vary', 'Accept')
res.setHeader(
'Cache-Control',
isStatic
Expand Down
28 changes: 28 additions & 0 deletions test/integration/image-optimizer/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ function runTests({ w, isDev, domains }) {
expect(res.headers.get('Cache-Control')).toBe(
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('Vary')).toBe('Accept')
expect(res.headers.get('etag')).toBeTruthy()
expect(isAnimated(await res.buffer())).toBe(true)
})
Expand All @@ -70,6 +71,7 @@ function runTests({ w, isDev, domains }) {
expect(res.headers.get('Cache-Control')).toBe(
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('Vary')).toBe('Accept')
expect(res.headers.get('etag')).toBeTruthy()
expect(isAnimated(await res.buffer())).toBe(true)
})
Expand All @@ -82,6 +84,7 @@ function runTests({ w, isDev, domains }) {
expect(res.headers.get('Cache-Control')).toBe(
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('Vary')).toBe('Accept')
expect(res.headers.get('etag')).toBeTruthy()
expect(isAnimated(await res.buffer())).toBe(true)
})
Expand All @@ -95,6 +98,9 @@ function runTests({ w, isDev, domains }) {
expect(res.headers.get('Cache-Control')).toBe(
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
// SVG is compressible so will have accept-encoding set from
// compression
expect(res.headers.get('Vary')).toMatch(/^Accept(,|$)/)
expect(res.headers.get('etag')).toBeTruthy()
const actual = await res.text()
const expected = await fs.readFile(
Expand All @@ -113,6 +119,7 @@ function runTests({ w, isDev, domains }) {
expect(res.headers.get('Cache-Control')).toBe(
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('Vary')).toMatch(/^Accept(,|$)/)
expect(res.headers.get('etag')).toBeTruthy()
const actual = await res.text()
const expected = await fs.readFile(
Expand All @@ -133,6 +140,7 @@ function runTests({ w, isDev, domains }) {
expect(res.headers.get('Cache-Control')).toBe(
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('Vary')).toBe('Accept')
expect(res.headers.get('etag')).toBeTruthy()
})

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

Expand Down Expand Up @@ -244,6 +253,7 @@ function runTests({ w, isDev, domains }) {
expect(res.headers.get('Cache-Control')).toBe(
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('Vary')).toBe('Accept')
expect(res.headers.get('etag')).toBeTruthy()
await expectWidth(res, w)
})
Expand All @@ -257,6 +267,7 @@ function runTests({ w, isDev, domains }) {
expect(res.headers.get('Cache-Control')).toBe(
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('Vary')).toBe('Accept')
expect(res.headers.get('etag')).toBeTruthy()
await expectWidth(res, w)
})
Expand All @@ -270,6 +281,7 @@ function runTests({ w, isDev, domains }) {
expect(res.headers.get('Cache-Control')).toBe(
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('Vary')).toBe('Accept')
expect(res.headers.get('etag')).toBeTruthy()
await expectWidth(res, w)
})
Expand All @@ -283,6 +295,7 @@ function runTests({ w, isDev, domains }) {
expect(res.headers.get('Cache-Control')).toBe(
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('Vary')).toBe('Accept')
expect(res.headers.get('etag')).toBeTruthy()
// FIXME: await expectWidth(res, w)
})
Expand All @@ -296,6 +309,7 @@ function runTests({ w, isDev, domains }) {
expect(res.headers.get('Cache-Control')).toBe(
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('Vary')).toBe('Accept')
expect(res.headers.get('etag')).toBeTruthy()
// FIXME: await expectWidth(res, w)
})
Expand All @@ -311,6 +325,7 @@ function runTests({ w, isDev, domains }) {
expect(res.headers.get('Cache-Control')).toBe(
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('Vary')).toBe('Accept')
expect(res.headers.get('etag')).toBeTruthy()
await expectWidth(res, w)
})
Expand All @@ -326,6 +341,7 @@ function runTests({ w, isDev, domains }) {
expect(res.headers.get('Cache-Control')).toBe(
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('Vary')).toBe('Accept')
expect(res.headers.get('etag')).toBeTruthy()
await expectWidth(res, w)
})
Expand All @@ -346,6 +362,7 @@ function runTests({ w, isDev, domains }) {
expect(res.headers.get('Cache-Control')).toBe(
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('Vary')).toBe('Accept')
expect(res.headers.get('etag')).toBeTruthy()
await expectWidth(res, w)
})
Expand Down Expand Up @@ -448,6 +465,7 @@ function runTests({ w, isDev, domains }) {
expect(res1.headers.get('Cache-Control')).toBe(
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res1.headers.get('Vary')).toBe('Accept')
const etag = res1.headers.get('Etag')
expect(etag).toBeTruthy()
await expectWidth(res1, w)
Expand All @@ -460,6 +478,7 @@ function runTests({ w, isDev, domains }) {
expect(res2.headers.get('Cache-Control')).toBe(
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res2.headers.get('Vary')).toBe('Accept')
expect((await res2.buffer()).length).toBe(0)

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

const json2 = await fsToJson(imagesDir)
Expand All @@ -501,6 +524,7 @@ function runTests({ w, isDev, domains }) {
expect(res.headers.get('Cache-Control')).toBe(
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('Vary')).toBe('Accept')
expect(res.headers.get('etag')).toBeTruthy()
await expectWidth(res, 400)
})
Expand All @@ -516,6 +540,7 @@ function runTests({ w, isDev, domains }) {
expect(res.headers.get('Cache-Control')).toBe(
`public, max-age=${isDev ? 0 : 60}, must-revalidate`
)
expect(res.headers.get('Vary')).toBe('Accept')

const png = await res.buffer()

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

// Ensure subsequent request also has immutable header
Expand All @@ -548,6 +574,7 @@ function runTests({ w, isDev, domains }) {
expect(res2.headers.get('Cache-Control')).toBe(
'public, max-age=315360000, immutable'
)
expect(res2.headers.get('Vary')).toBe('Accept')
await expectWidth(res2, w)
}
})
Expand Down Expand Up @@ -873,6 +900,7 @@ describe('Image Optimizer', () => {
expect(res.headers.get('Cache-Control')).toBe(
`public, max-age=31536000, must-revalidate`
)
expect(res.headers.get('Vary')).toBe('Accept')
await expectWidth(res, 64)
})
})
Expand Down

0 comments on commit d670198

Please sign in to comment.