From a94c833a6eaf533fa5087b456a9fea8e0e92c8bb Mon Sep 17 00:00:00 2001 From: Fastering18 Date: Sun, 9 Aug 2026 11:10:15 +0700 Subject: [PATCH] fix(next/image): include response status in invalid image errors Fixes #82357 --- packages/next/src/server/image-optimizer.ts | 15 ++++- .../fetch-internal-image.test.ts | 63 +++++++++++++++++++ ...et-previously-cached-image-or-null.test.ts | 1 + 3 files changed, 76 insertions(+), 3 deletions(-) diff --git a/packages/next/src/server/image-optimizer.ts b/packages/next/src/server/image-optimizer.ts index 5388bc4ac74b..76529fe5c454 100644 --- a/packages/next/src/server/image-optimizer.ts +++ b/packages/next/src/server/image-optimizer.ts @@ -143,6 +143,7 @@ interface ImageUpstream { contentType: string | null | undefined cacheControl: string | null | undefined etag: string + statusCode: number } function getSupportedMimeType(options: string[], accept = ''): string { @@ -961,7 +962,7 @@ export async function fetchExternalImage( const contentType = res.headers.get('Content-Type') const cacheControl = res.headers.get('Cache-Control') const etag = extractEtag(res.headers.get('ETag'), buffer) - return { buffer, contentType, cacheControl, etag } + return { buffer, contentType, cacheControl, etag, statusCode: res.status } } export async function fetchInternalImage( @@ -1010,7 +1011,13 @@ export async function fetchInternalImage( const cacheControl = mocked.res.getHeader('Cache-Control') const etag = extractEtag(mocked.res.getHeader('ETag'), buffer) - return { buffer, contentType, cacheControl, etag } + return { + buffer, + contentType, + cacheControl, + etag, + statusCode: mocked.res.statusCode, + } } catch (err) { if (err instanceof ImageError) { throw err @@ -1089,7 +1096,9 @@ export async function imageOptimizer( "The requested resource isn't a valid image for", href, 'received', - upstreamType + upstreamType, + 'with status', + imageUpstream.statusCode ) } throw new ImageError(400, "The requested resource isn't a valid image.") diff --git a/test/unit/image-optimizer/fetch-internal-image.test.ts b/test/unit/image-optimizer/fetch-internal-image.test.ts index e9acf219aa61..dc1ed3dfe03f 100644 --- a/test/unit/image-optimizer/fetch-internal-image.test.ts +++ b/test/unit/image-optimizer/fetch-internal-image.test.ts @@ -1,6 +1,7 @@ /* eslint-env jest */ import { fetchInternalImage, + imageOptimizer, ImageError, } from 'next/dist/server/image-optimizer' import type { IncomingMessage, ServerResponse } from 'http' @@ -149,4 +150,66 @@ describe('fetchInternalImage', () => { expect(result.buffer.length).toBe(maximumResponseBody) }) }) + + it('should include the response status when the image type is invalid', async () => { + const mockReq = {} as IncomingMessage + const mockRes = {} as ServerResponse + const handleRequest = jest.fn( + async (_req: IncomingMessage, res: ServerResponse) => { + res.statusCode = 307 + res.write('Redirecting') + res.end() + } + ) + + const upstreamImage = await fetchInternalImage( + '/redirected-image.png', + mockReq, + mockRes, + 50_000_000, + handleRequest + ) + + expect(upstreamImage).toMatchObject({ + contentType: undefined, + statusCode: 307, + }) + + const consoleError = jest + .spyOn(console, 'error') + .mockImplementation(() => {}) + + try { + const error = await imageOptimizer( + upstreamImage, + { + href: '/redirected-image.png', + width: 640, + quality: 75, + mimeType: 'image/webp', + }, + { + experimental: {}, + images: { + dangerouslyAllowSVG: false, + minimumCacheTTL: 60, + }, + } as Parameters[2], + {} + ).catch((e) => e) + + expect(error).toBeInstanceOf(ImageError) + expect(consoleError).toHaveBeenCalledWith( + expect.any(String), + "The requested resource isn't a valid image for", + '/redirected-image.png', + 'received', + null, + 'with status', + 307 + ) + } finally { + consoleError.mockRestore() + } + }) }) diff --git a/test/unit/image-optimizer/get-previously-cached-image-or-null.test.ts b/test/unit/image-optimizer/get-previously-cached-image-or-null.test.ts index c0d8e2230aa1..6f2fd44ab138 100644 --- a/test/unit/image-optimizer/get-previously-cached-image-or-null.test.ts +++ b/test/unit/image-optimizer/get-previously-cached-image-or-null.test.ts @@ -17,6 +17,7 @@ const getImageUpstream = async (filepath, contentType = 'image/jpeg') => { contentType, cacheControl: 'max-age=31536000', etag: getImageEtag(buffer), + statusCode: 200, } return result }