@@ -16,6 +16,7 @@ import {
1616 waitFor ,
1717} from 'next-test-utils'
1818import isAnimated from 'next/dist/compiled/is-animated'
19+ import { getSharp , isAvifDecodeSafe } from 'next/dist/server/image-optimizer'
1920import type { RequestInit } from 'node-fetch'
2021import type { NextConfig } from 'next'
2122
@@ -100,6 +101,26 @@ export async function expectWidth(res, w, { expectAnimated = false } = {}) {
100101 expect ( isAnimated ( buffer ) ) . toBe ( expectAnimated )
101102}
102103
104+ async function expectUnoptimizedImage ( res , filename : string ) {
105+ const buffer = Buffer . from ( await res . arrayBuffer ( ) )
106+ const source = await fs . readFile ( join ( __dirname , '../app/public' , filename ) )
107+ expect ( buffer ) . toEqual ( source )
108+ expect ( res . headers . get ( 'Content-Length' ) ) . toBe (
109+ Buffer . byteLength ( source ) . toString ( )
110+ )
111+ }
112+
113+ // The image optimizer only decodes AVIF input when the installed sharp
114+ // bundles a patched libheif. The same check decides the expectations here so
115+ // CI jobs pinned to the vulnerable 0.34 line (Node 18) assert the bypass.
116+ function isAvifInputOptimizable ( ) : boolean {
117+ try {
118+ return isAvifDecodeSafe ( getSharp ( null ) . versions ?. heif ?? null )
119+ } catch {
120+ return false
121+ }
122+ }
123+
103124export const cleanImagesDir = async ( ctx : { imagesDir : string } ) => {
104125 console . warn ( 'Cleaning' , ctx . imagesDir )
105126 await fs . remove ( ctx . imagesDir )
@@ -530,16 +551,24 @@ export function runTests(ctx: RunTestsCtx) {
530551 const opts = { headers : { accept } }
531552 const res = await fetchViaHTTP ( ctx . appPort , '/_next/image' , query , opts )
532553 expect ( res . status ) . toBe ( 200 )
533- expect ( res . headers . get ( 'Content-Type' ) ) . toContain ( 'image/jpeg' )
534554 expect ( res . headers . get ( 'Cache-Control' ) ) . toBe (
535555 `public, max-age=${ isDev ? 0 : minimumCacheTTL } , must-revalidate`
536556 )
537557 expect ( res . headers . get ( 'Vary' ) ) . toBe ( 'Accept' )
538558 expect ( res . headers . get ( 'etag' ) ) . toBeTruthy ( )
539- expect ( res . headers . get ( 'Content-Disposition' ) ) . toBe (
540- `${ contentDispositionType } ; filename="test.jpeg"`
541- )
542- await expectWidth ( res , ctx . w )
559+ if ( isAvifInputOptimizable ( ) ) {
560+ expect ( res . headers . get ( 'Content-Type' ) ) . toContain ( 'image/jpeg' )
561+ expect ( res . headers . get ( 'Content-Disposition' ) ) . toBe (
562+ `${ contentDispositionType } ; filename="test.jpeg"`
563+ )
564+ await expectWidth ( res , ctx . w )
565+ } else {
566+ expect ( res . headers . get ( 'Content-Type' ) ) . toContain ( 'image/avif' )
567+ expect ( res . headers . get ( 'Content-Disposition' ) ) . toBe (
568+ `${ contentDispositionType } ; filename="test.avif"`
569+ )
570+ await expectUnoptimizedImage ( res , 'test.avif' )
571+ }
543572 } )
544573
545574 it ( 'should fail when url is missing' , async ( ) => {
@@ -810,16 +839,24 @@ export function runTests(ctx: RunTestsCtx) {
810839 const opts = { headers : { accept : 'image/webp' } }
811840 const res = await fetchViaHTTP ( ctx . appPort , '/_next/image' , query , opts )
812841 expect ( res . status ) . toBe ( 200 )
813- expect ( res . headers . get ( 'Content-Type' ) ) . toBe ( 'image/webp' )
814842 expect ( res . headers . get ( 'Cache-Control' ) ) . toBe (
815843 `public, max-age=${ isDev ? 0 : minimumCacheTTL } , must-revalidate`
816844 )
817845 expect ( res . headers . get ( 'Vary' ) ) . toBe ( 'Accept' )
818846 expect ( res . headers . get ( 'etag' ) ) . toBeTruthy ( )
819- expect ( res . headers . get ( 'Content-Disposition' ) ) . toBe (
820- `${ contentDispositionType } ; filename="test.webp"`
821- )
822- await expectWidth ( res , ctx . w )
847+ if ( isAvifInputOptimizable ( ) ) {
848+ expect ( res . headers . get ( 'Content-Type' ) ) . toBe ( 'image/webp' )
849+ expect ( res . headers . get ( 'Content-Disposition' ) ) . toBe (
850+ `${ contentDispositionType } ; filename="test.webp"`
851+ )
852+ await expectWidth ( res , ctx . w )
853+ } else {
854+ expect ( res . headers . get ( 'Content-Type' ) ) . toBe ( 'image/avif' )
855+ expect ( res . headers . get ( 'Content-Disposition' ) ) . toBe (
856+ `${ contentDispositionType } ; filename="test.avif"`
857+ )
858+ await expectUnoptimizedImage ( res , 'test.avif' )
859+ }
823860 } )
824861
825862 it ( 'should resize relative url and old Chrome accept header as webp' , async ( ) => {
@@ -877,7 +914,11 @@ export function runTests(ctx: RunTestsCtx) {
877914 expect ( res . headers . get ( 'Content-Disposition' ) ) . toBe (
878915 `${ contentDispositionType } ; filename="test.avif"`
879916 )
880- await expectWidth ( res , ctx . w )
917+ if ( isAvifInputOptimizable ( ) ) {
918+ await expectWidth ( res , ctx . w )
919+ } else {
920+ await expectUnoptimizedImage ( res , 'test.avif' )
921+ }
881922 } )
882923
883924 it ( 'should compress avif smaller than webp at q=100' , async ( ) => {
0 commit comments