diff --git a/packages/next/server/image-optimizer.ts b/packages/next/server/image-optimizer.ts index 976bd8c29fd6..9adec2dd8a1d 100644 --- a/packages/next/server/image-optimizer.ts +++ b/packages/next/server/image-optimizer.ts @@ -419,7 +419,12 @@ export async function imageOptimizer( if (mimeType) { contentType = mimeType - } else if (upstreamType?.startsWith('image/') && getExtension(upstreamType)) { + } else if ( + upstreamType?.startsWith('image/') && + getExtension(upstreamType) && + upstreamType !== WEBP && + upstreamType !== AVIF + ) { contentType = upstreamType } else { contentType = JPEG diff --git a/test/integration/image-optimizer/app/public/test.avif b/test/integration/image-optimizer/app/public/test.avif new file mode 100644 index 000000000000..e2c8170a6833 Binary files /dev/null and b/test/integration/image-optimizer/app/public/test.avif differ diff --git a/test/integration/image-optimizer/app/public/test.webp b/test/integration/image-optimizer/app/public/test.webp new file mode 100644 index 000000000000..4b306cb0898c Binary files /dev/null and b/test/integration/image-optimizer/app/public/test.webp differ diff --git a/test/integration/image-optimizer/test/util.js b/test/integration/image-optimizer/test/util.js index 51c57a75552d..ee83f9e93b18 100644 --- a/test/integration/image-optimizer/test/util.js +++ b/test/integration/image-optimizer/test/util.js @@ -313,6 +313,44 @@ export function runTests(ctx) { ) }) + it('should downlevel webp format to jpeg for old Safari', async () => { + const accept = + 'image/png,image/svg+xml,image/*;q=0.8,video/*;q=0.8,*/*;q=0.5' + const query = { w: ctx.w, q: 74, url: '/test.webp' } + const opts = { headers: { accept } } + const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + expect(res.status).toBe(200) + expect(res.headers.get('Content-Type')).toContain('image/jpeg') + expect(res.headers.get('Cache-Control')).toBe( + `public, max-age=0, must-revalidate` + ) + expect(res.headers.get('Vary')).toBe('Accept') + expect(res.headers.get('etag')).toBeTruthy() + expect(res.headers.get('Content-Disposition')).toBe( + `inline; filename="test.jpeg"` + ) + }) + + if (!ctx.isOutdatedSharp) { + it('should downlevel avif format to jpeg for old Safari', async () => { + const accept = + 'image/png,image/svg+xml,image/*;q=0.8,video/*;q=0.8,*/*;q=0.5' + const query = { w: ctx.w, q: 74, url: '/test.avif' } + const opts = { headers: { accept } } + const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, opts) + expect(res.status).toBe(200) + expect(res.headers.get('Content-Type')).toContain('image/jpeg') + expect(res.headers.get('Cache-Control')).toBe( + `public, max-age=0, must-revalidate` + ) + expect(res.headers.get('Vary')).toBe('Accept') + expect(res.headers.get('etag')).toBeTruthy() + expect(res.headers.get('Content-Disposition')).toBe( + `inline; filename="test.jpeg"` + ) + }) + } + it('should fail when url is missing', async () => { const query = { w: ctx.w, q: 100 } const res = await fetchViaHTTP(ctx.appPort, '/_next/image', query, {})