From cb981b67af55efa5457cfcbea611af56539a0e5a Mon Sep 17 00:00:00 2001 From: Nicholas Carter Date: Wed, 13 May 2026 15:10:05 +0100 Subject: [PATCH 1/3] chore: refactor image optimization replacement to work correctly with various versions --- .changeset/tired-phones-cover.md | 5 + .../image-optimization.replacement.ts | 119 ++++++---- .../tests/adapters/image-optimization.test.ts | 219 ++++++++++++++++++ 3 files changed, 294 insertions(+), 49 deletions(-) create mode 100644 .changeset/tired-phones-cover.md create mode 100644 packages/tests-unit/tests/adapters/image-optimization.test.ts diff --git a/.changeset/tired-phones-cover.md b/.changeset/tired-phones-cover.md new file mode 100644 index 000000000..ba6286d9f --- /dev/null +++ b/.changeset/tired-phones-cover.md @@ -0,0 +1,5 @@ +--- +"@opennextjs/aws": patch +--- + +Refactor image optimization replacement function to be more readable diff --git a/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.replacement.ts b/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.replacement.ts index 7af3128d6..d79772dfc 100644 --- a/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.replacement.ts +++ b/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.replacement.ts @@ -27,61 +27,82 @@ export async function optimizeImage( ) { const { isAbsolute, href } = imageParams; - // Signature for the fetch Image functions have changed across Next.js versions. - const isV15After15510 = - compareSemver(globalThis.nextVersion, ">=", "15.5.10") && - compareSemver(globalThis.nextVersion, "<", "16"); - const isV15After15515 = - compareSemver(globalThis.nextVersion, ">=", "15.5.16") && - compareSemver(globalThis.nextVersion, "<", "16"); - const isV16Plus = compareSemver(globalThis.nextVersion, ">=", "16"); - const isAfter1615 = compareSemver(globalThis.nextVersion, ">=", "16.1.5"); - const isAfter1625 = compareSemver(globalThis.nextVersion, ">=", "16.2.5"); - - // fetchInternalImage signature varies across Next.js versions: - // <=v15.5.15, v16.0.0–v16.2.4: fetchInternalImage(href, req, res, handleRequest) - // v15.5.16–v15.x, v16.2.5+: fetchInternalImage(href, req, res, maximumResponseBody, handleRequest) - const isNewArgsForInternalFetch = isAfter1625 || isV15After15515; - - // fetchExternalImage signature varies across Next.js versions: - // <=v15.5.9: fetchExternalImage(href) - // v15.5.10–v15.x: fetchExternalImage(href, maximumResponseBody) - // v16.0.0–v16.1.4: fetchExternalImage(href, dangerouslyAllowLocalIP) - // v16.1.5+: fetchExternalImage(href, dangerouslyAllowLocalIP, maximumResponseBody) - // The default value for maximumResponseBody is 50KB in Next code // https://github.com/vercel/next.js/blob/0f38c522/packages/next/src/shared/lib/image-config.ts#L164 const maximumResponseBody = nextConfig.images?.maximumResponseBody || 50_000_000; + /** + * fetchExternalImage signature varies across Next.js versions: + * <=v15.5.9: fetchExternalImage(href) + * v15.5.10–v15.x: fetchExternalImage(href, maximumResponseBody) + * v16.0.0–v16.1.4: fetchExternalImage(href, dangerouslyAllowLocalIP) + * v16.1.5+: fetchExternalImage(href, dangerouslyAllowLocalIP, maximumResponseBody) + * + * https://github.com/vercel/next.js/blob/bfe2ab4/packages/next/src/server/image-optimizer.ts#L711 + */ + const callFetchExternalImage = () => { + const version = globalThis.nextVersion; + + // v16.1.5+ + if (compareSemver(version, ">=", "16.1.5")) { + return fetchExternalImage(href, false, maximumResponseBody); + } + + // v16.0.0–v16.1.4 + if (compareSemver(version, ">=", "16")) { + // @ts-expect-error - fetchExternalImage signature varies across Next.js versions + return fetchExternalImage(href, false); + } + + // v15.5.10–v15.x + if (compareSemver(version, ">=", "15.5.10")) { + // @ts-expect-error - fetchExternalImage signature varies across Next.js versions + return fetchExternalImage(href, maximumResponseBody); + } + + // <=v15.5.9 + // @ts-expect-error - fetchExternalImage signature varies across Next.js versions + return fetchExternalImage(href); + }; + + /** + * fetchInternalImage signature varies across Next.js versions: + * <=v15.5.15, v16.0.0–v16.2.4: fetchInternalImage(href, req, res, handleRequest) + * v15.5.16–v15.x, v16.2.5+: fetchInternalImage(href, req, res, maximumResponseBody, handleRequest) + */ + const callFetchInternalImage = () => { + const version = globalThis.nextVersion; + + const isV15WithNewArgs = + compareSemver(version, ">=", "15.5.16") && + compareSemver(version, "<", "16"); + const isV16WithNewArgs = compareSemver(version, ">=", "16.2.5"); + const useNewArgs = isV15WithNewArgs || isV16WithNewArgs; + + if (useNewArgs) { + return fetchInternalImage( + href, + // @ts-expect-error - It is supposed to be an IncomingMessage object, but only the headers are used. + { headers }, + {}, // res object is not necessary as it's not actually used. + maximumResponseBody, + handleRequest, + ); + } + + // @ts-expect-error - fetchInternalImage signature has changed in Next.js 15.5.16 and 16.2.5 + return fetchInternalImage( + href, + { headers }, + {}, // res object is not necessary as it's not actually used. + handleRequest, + ); + }; + const imageUpstream = isAbsolute - ? // https://github.com/vercel/next.js/blob/bfe2ab4/packages/next/src/server/image-optimizer.ts#L711 - await (isAfter1615 - ? fetchExternalImage(href, false, maximumResponseBody) - : isV16Plus - ? // @ts-expect-error - fetchExternalImage signature varies across Next.js versions - fetchExternalImage(href, false) - : isV15After15510 - ? // @ts-expect-error - fetchExternalImage signature varies across Next.js versions - fetchExternalImage(href, maximumResponseBody) - : // @ts-expect-error - fetchExternalImage signature varies across Next.js versions - fetchExternalImage(href)) - : await (isNewArgsForInternalFetch - ? fetchInternalImage( - href, - // @ts-expect-error - It is supposed to be an IncomingMessage object, but only the headers are used. - { headers }, - {}, // res object is not necessary as it's not actually used. - maximumResponseBody, - handleRequest, - ) - : // @ts-expect-error - fetchInternalImage signature has changed in Next.js 15.5.16 and 16.2.5 - fetchInternalImage( - href, - { headers }, - {}, // res object is not necessary as it's not actually used. - handleRequest, - )); + ? await callFetchExternalImage() + : await callFetchInternalImage(); const result = await imageOptimizer( imageUpstream, diff --git a/packages/tests-unit/tests/adapters/image-optimization.test.ts b/packages/tests-unit/tests/adapters/image-optimization.test.ts new file mode 100644 index 000000000..3d283709b --- /dev/null +++ b/packages/tests-unit/tests/adapters/image-optimization.test.ts @@ -0,0 +1,219 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +// The file under test imports from `next/dist/server/image-optimizer` which +// is only available at runtime inside a user's built Next.js app. We mock the +// module so the tests can run independently of any Next.js installation. +const mockFetchExternalImage = vi.fn(); +const mockFetchInternalImage = vi.fn(); +const mockImageOptimizer = vi.fn(); + +vi.mock("next/dist/server/image-optimizer", () => ({ + fetchExternalImage: (...args: unknown[]) => mockFetchExternalImage(...args), + fetchInternalImage: (...args: unknown[]) => mockFetchInternalImage(...args), + imageOptimizer: (...args: unknown[]) => mockImageOptimizer(...args), +})); + +vi.mock("@opennextjs/aws/adapters/logger.js", () => ({ + debug: vi.fn(), +})); + +// Imported after the mocks are registered so the module uses the mocked +// versions. +const { optimizeImage } = await import( + "@opennextjs/aws/adapters/plugins/image-optimization/image-optimization.replacement.js" +); + +declare global { + var nextVersion: string; +} + +const HREF = "/some/image.png"; +const UPSTREAM = { buffer: Buffer.from(""), contentType: "image/png" }; +const OPTIMIZED = { + buffer: Buffer.from("optimized"), + contentType: "image/png", +}; + +const handleRequest = vi.fn(); + +function callOptimizeImage( + overrides: { + isAbsolute?: boolean; + maximumResponseBody?: number; + } = {}, +) { + const { isAbsolute = false, maximumResponseBody } = overrides; + const headers = { "x-custom": "value" }; + const imageParams = { isAbsolute, href: HREF }; + const nextConfig = { + images: maximumResponseBody ? { maximumResponseBody } : {}, + } as any; + + return optimizeImage(headers, imageParams, nextConfig, handleRequest); +} + +describe("optimizeImage (replacement)", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockFetchExternalImage.mockResolvedValue(UPSTREAM); + mockFetchInternalImage.mockResolvedValue(UPSTREAM); + mockImageOptimizer.mockResolvedValue(OPTIMIZED); + }); + + describe("fetchExternalImage (isAbsolute=true)", () => { + it("calls fetchExternalImage(href) for <= v15.5.9", async () => { + globalThis.nextVersion = "15.5.9"; + await callOptimizeImage({ isAbsolute: true }); + expect(mockFetchExternalImage).toHaveBeenCalledTimes(1); + expect(mockFetchExternalImage).toHaveBeenCalledWith(HREF); + }); + + it("calls fetchExternalImage(href, maximumResponseBody) for v15.5.10", async () => { + globalThis.nextVersion = "15.5.10"; + await callOptimizeImage({ isAbsolute: true }); + expect(mockFetchExternalImage).toHaveBeenCalledWith(HREF, 50_000_000); + }); + + it("calls fetchExternalImage(href, maximumResponseBody) for v15.5.15", async () => { + globalThis.nextVersion = "15.5.15"; + await callOptimizeImage({ isAbsolute: true }); + expect(mockFetchExternalImage).toHaveBeenCalledWith(HREF, 50_000_000); + }); + + it("calls fetchExternalImage(href, maximumResponseBody) for v15.5.16", async () => { + // Internal fetch args change at 15.5.16, but external args stay + // the same as 15.5.10+ until v16. + globalThis.nextVersion = "15.5.16"; + await callOptimizeImage({ isAbsolute: true }); + expect(mockFetchExternalImage).toHaveBeenCalledWith(HREF, 50_000_000); + }); + + it("calls fetchExternalImage(href, false) for v16.0.0", async () => { + globalThis.nextVersion = "16.0.0"; + await callOptimizeImage({ isAbsolute: true }); + expect(mockFetchExternalImage).toHaveBeenCalledWith(HREF, false); + }); + + it("calls fetchExternalImage(href, false) for v16.1.4", async () => { + globalThis.nextVersion = "16.1.4"; + await callOptimizeImage({ isAbsolute: true }); + expect(mockFetchExternalImage).toHaveBeenCalledWith(HREF, false); + }); + + it("calls fetchExternalImage(href, false, maximumResponseBody) for v16.1.5", async () => { + globalThis.nextVersion = "16.1.5"; + await callOptimizeImage({ isAbsolute: true }); + expect(mockFetchExternalImage).toHaveBeenCalledWith( + HREF, + false, + 50_000_000, + ); + }); + + it("calls fetchExternalImage(href, false, maximumResponseBody) for v16.2.5", async () => { + globalThis.nextVersion = "16.2.5"; + await callOptimizeImage({ isAbsolute: true }); + expect(mockFetchExternalImage).toHaveBeenCalledWith( + HREF, + false, + 50_000_000, + ); + }); + + it("forwards a custom maximumResponseBody from nextConfig", async () => { + globalThis.nextVersion = "16.2.5"; + await callOptimizeImage({ + isAbsolute: true, + maximumResponseBody: 1_234, + }); + expect(mockFetchExternalImage).toHaveBeenCalledWith(HREF, false, 1_234); + }); + + it("does not call fetchInternalImage", async () => { + globalThis.nextVersion = "16.2.5"; + await callOptimizeImage({ isAbsolute: true }); + expect(mockFetchInternalImage).not.toHaveBeenCalled(); + }); + }); + + describe("fetchInternalImage (isAbsolute=false)", () => { + it("uses the old signature for v15.5.15", async () => { + globalThis.nextVersion = "15.5.15"; + await callOptimizeImage({ isAbsolute: false }); + expect(mockFetchInternalImage).toHaveBeenCalledTimes(1); + expect(mockFetchInternalImage).toHaveBeenCalledWith( + HREF, + { headers: { "x-custom": "value" } }, + {}, + handleRequest, + ); + }); + + it("uses the new signature for v15.5.16 (with maximumResponseBody)", async () => { + globalThis.nextVersion = "15.5.16"; + await callOptimizeImage({ isAbsolute: false }); + expect(mockFetchInternalImage).toHaveBeenCalledWith( + HREF, + { headers: { "x-custom": "value" } }, + {}, + 50_000_000, + handleRequest, + ); + }); + + it("uses the old signature for v16.0.0 (< 16.2.5)", async () => { + globalThis.nextVersion = "16.0.0"; + await callOptimizeImage({ isAbsolute: false }); + expect(mockFetchInternalImage).toHaveBeenCalledWith( + HREF, + { headers: { "x-custom": "value" } }, + {}, + handleRequest, + ); + }); + + it("uses the old signature for v16.2.4", async () => { + globalThis.nextVersion = "16.2.4"; + await callOptimizeImage({ isAbsolute: false }); + expect(mockFetchInternalImage).toHaveBeenCalledWith( + HREF, + { headers: { "x-custom": "value" } }, + {}, + handleRequest, + ); + }); + + it("uses the new signature for v16.2.5+", async () => { + globalThis.nextVersion = "16.2.5"; + await callOptimizeImage({ isAbsolute: false }); + expect(mockFetchInternalImage).toHaveBeenCalledWith( + HREF, + { headers: { "x-custom": "value" } }, + {}, + 50_000_000, + handleRequest, + ); + }); + + it("does not call fetchExternalImage", async () => { + globalThis.nextVersion = "16.2.5"; + await callOptimizeImage({ isAbsolute: false }); + expect(mockFetchExternalImage).not.toHaveBeenCalled(); + }); + }); + + describe("imageOptimizer", () => { + it("is called with the upstream result and returns its value", async () => { + globalThis.nextVersion = "16.2.5"; + const result = await callOptimizeImage({ isAbsolute: true }); + expect(mockImageOptimizer).toHaveBeenCalledTimes(1); + expect(mockImageOptimizer).toHaveBeenCalledWith( + UPSTREAM, + { isAbsolute: true, href: HREF }, + expect.any(Object), + false, + ); + expect(result).toBe(OPTIMIZED); + }); + }); +}); From 3e57683bead2de0b2a060d808004ec09e50cc369 Mon Sep 17 00:00:00 2001 From: Nicholas Carter Date: Wed, 13 May 2026 15:16:39 +0100 Subject: [PATCH 2/3] chore: refactor internalImage call --- .../image-optimization.replacement.ts | 148 ++++++++++-------- 1 file changed, 81 insertions(+), 67 deletions(-) diff --git a/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.replacement.ts b/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.replacement.ts index d79772dfc..f72434e22 100644 --- a/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.replacement.ts +++ b/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.replacement.ts @@ -14,6 +14,83 @@ import type { NextUrlWithParsedQuery } from "next/dist/server/request-meta"; import { compareSemver } from "utils/semver.js"; import { debug } from "../../logger.js"; +/** + * fetchExternalImage signature varies across Next.js versions: + * <=v15.5.9: fetchExternalImage(href) + * v15.5.10–v15.x: fetchExternalImage(href, maximumResponseBody) + * v16.0.0–v16.1.4: fetchExternalImage(href, dangerouslyAllowLocalIP) + * v16.1.5+: fetchExternalImage(href, dangerouslyAllowLocalIP, maximumResponseBody) + * + * https://github.com/vercel/next.js/blob/bfe2ab4/packages/next/src/server/image-optimizer.ts#L711 + */ +function callFetchExternalImage(href: string, maximumResponseBody: number) { + const version = globalThis.nextVersion; + + // v16.1.5+ + if (compareSemver(version, ">=", "16.1.5")) { + return fetchExternalImage(href, false, maximumResponseBody); + } + + // v16.0.0–v16.1.4 + if (compareSemver(version, ">=", "16")) { + // @ts-expect-error - fetchExternalImage signature varies across Next.js versions + return fetchExternalImage(href, false); + } + + // v15.5.10–v15.x + if (compareSemver(version, ">=", "15.5.10")) { + // @ts-expect-error - fetchExternalImage signature varies across Next.js versions + return fetchExternalImage(href, maximumResponseBody); + } + + // <=v15.5.9 + // @ts-expect-error - fetchExternalImage signature varies across Next.js versions + return fetchExternalImage(href); +} + +/** + * fetchInternalImage signature varies across Next.js versions: + * <=v15.5.15, v16.0.0–v16.2.4: fetchInternalImage(href, req, res, handleRequest) + * v15.5.16–v15.x, v16.2.5+: fetchInternalImage(href, req, res, maximumResponseBody, handleRequest) + */ +function callFetchInternalImage( + href: string, + headers: APIGatewayProxyEventHeaders, + maximumResponseBody: number, + handleRequest: ( + newReq: IncomingMessage, + newRes: ServerResponse, + newParsedUrl?: NextUrlWithParsedQuery, + ) => Promise, +) { + const version = globalThis.nextVersion; + + const isV15WithNewArgs = + compareSemver(version, ">=", "15.5.16") && + compareSemver(version, "<", "16"); + + // v15.5.16–v15.x, v16.2.5+ + if (isV15WithNewArgs || compareSemver(version, ">=", "16.2.5")) { + return fetchInternalImage( + href, + // @ts-expect-error - It is supposed to be an IncomingMessage object, but only the headers are used. + { headers }, + {}, // res object is not necessary as it's not actually used. + maximumResponseBody, + handleRequest, + ); + } + + // <=v15.5.15, v16.0.0–v16.2.4 + // @ts-expect-error - fetchInternalImage signature has changed in Next.js 15.5.16 and 16.2.5 + return fetchInternalImage( + href, + { headers }, + {}, // res object is not necessary as it's not actually used. + handleRequest, + ); +} + //#override optimizeImage export async function optimizeImage( headers: APIGatewayProxyEventHeaders, @@ -32,77 +109,14 @@ export async function optimizeImage( const maximumResponseBody = nextConfig.images?.maximumResponseBody || 50_000_000; - /** - * fetchExternalImage signature varies across Next.js versions: - * <=v15.5.9: fetchExternalImage(href) - * v15.5.10–v15.x: fetchExternalImage(href, maximumResponseBody) - * v16.0.0–v16.1.4: fetchExternalImage(href, dangerouslyAllowLocalIP) - * v16.1.5+: fetchExternalImage(href, dangerouslyAllowLocalIP, maximumResponseBody) - * - * https://github.com/vercel/next.js/blob/bfe2ab4/packages/next/src/server/image-optimizer.ts#L711 - */ - const callFetchExternalImage = () => { - const version = globalThis.nextVersion; - - // v16.1.5+ - if (compareSemver(version, ">=", "16.1.5")) { - return fetchExternalImage(href, false, maximumResponseBody); - } - - // v16.0.0–v16.1.4 - if (compareSemver(version, ">=", "16")) { - // @ts-expect-error - fetchExternalImage signature varies across Next.js versions - return fetchExternalImage(href, false); - } - - // v15.5.10–v15.x - if (compareSemver(version, ">=", "15.5.10")) { - // @ts-expect-error - fetchExternalImage signature varies across Next.js versions - return fetchExternalImage(href, maximumResponseBody); - } - - // <=v15.5.9 - // @ts-expect-error - fetchExternalImage signature varies across Next.js versions - return fetchExternalImage(href); - }; - - /** - * fetchInternalImage signature varies across Next.js versions: - * <=v15.5.15, v16.0.0–v16.2.4: fetchInternalImage(href, req, res, handleRequest) - * v15.5.16–v15.x, v16.2.5+: fetchInternalImage(href, req, res, maximumResponseBody, handleRequest) - */ - const callFetchInternalImage = () => { - const version = globalThis.nextVersion; - - const isV15WithNewArgs = - compareSemver(version, ">=", "15.5.16") && - compareSemver(version, "<", "16"); - const isV16WithNewArgs = compareSemver(version, ">=", "16.2.5"); - const useNewArgs = isV15WithNewArgs || isV16WithNewArgs; - - if (useNewArgs) { - return fetchInternalImage( + const imageUpstream = isAbsolute + ? await callFetchExternalImage(href, maximumResponseBody) + : await callFetchInternalImage( href, - // @ts-expect-error - It is supposed to be an IncomingMessage object, but only the headers are used. - { headers }, - {}, // res object is not necessary as it's not actually used. + headers, maximumResponseBody, handleRequest, ); - } - - // @ts-expect-error - fetchInternalImage signature has changed in Next.js 15.5.16 and 16.2.5 - return fetchInternalImage( - href, - { headers }, - {}, // res object is not necessary as it's not actually used. - handleRequest, - ); - }; - - const imageUpstream = isAbsolute - ? await callFetchExternalImage() - : await callFetchInternalImage(); const result = await imageOptimizer( imageUpstream, From 5234700c84e6863066ea57ac793fb26c33a36522 Mon Sep 17 00:00:00 2001 From: Nicholas Carter Date: Wed, 13 May 2026 15:19:32 +0100 Subject: [PATCH 3/3] chore: move functions back into optimizeImage --- .../image-optimization.replacement.ts | 148 ++++++++---------- 1 file changed, 67 insertions(+), 81 deletions(-) diff --git a/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.replacement.ts b/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.replacement.ts index f72434e22..e990cd7c2 100644 --- a/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.replacement.ts +++ b/packages/open-next/src/adapters/plugins/image-optimization/image-optimization.replacement.ts @@ -14,83 +14,6 @@ import type { NextUrlWithParsedQuery } from "next/dist/server/request-meta"; import { compareSemver } from "utils/semver.js"; import { debug } from "../../logger.js"; -/** - * fetchExternalImage signature varies across Next.js versions: - * <=v15.5.9: fetchExternalImage(href) - * v15.5.10–v15.x: fetchExternalImage(href, maximumResponseBody) - * v16.0.0–v16.1.4: fetchExternalImage(href, dangerouslyAllowLocalIP) - * v16.1.5+: fetchExternalImage(href, dangerouslyAllowLocalIP, maximumResponseBody) - * - * https://github.com/vercel/next.js/blob/bfe2ab4/packages/next/src/server/image-optimizer.ts#L711 - */ -function callFetchExternalImage(href: string, maximumResponseBody: number) { - const version = globalThis.nextVersion; - - // v16.1.5+ - if (compareSemver(version, ">=", "16.1.5")) { - return fetchExternalImage(href, false, maximumResponseBody); - } - - // v16.0.0–v16.1.4 - if (compareSemver(version, ">=", "16")) { - // @ts-expect-error - fetchExternalImage signature varies across Next.js versions - return fetchExternalImage(href, false); - } - - // v15.5.10–v15.x - if (compareSemver(version, ">=", "15.5.10")) { - // @ts-expect-error - fetchExternalImage signature varies across Next.js versions - return fetchExternalImage(href, maximumResponseBody); - } - - // <=v15.5.9 - // @ts-expect-error - fetchExternalImage signature varies across Next.js versions - return fetchExternalImage(href); -} - -/** - * fetchInternalImage signature varies across Next.js versions: - * <=v15.5.15, v16.0.0–v16.2.4: fetchInternalImage(href, req, res, handleRequest) - * v15.5.16–v15.x, v16.2.5+: fetchInternalImage(href, req, res, maximumResponseBody, handleRequest) - */ -function callFetchInternalImage( - href: string, - headers: APIGatewayProxyEventHeaders, - maximumResponseBody: number, - handleRequest: ( - newReq: IncomingMessage, - newRes: ServerResponse, - newParsedUrl?: NextUrlWithParsedQuery, - ) => Promise, -) { - const version = globalThis.nextVersion; - - const isV15WithNewArgs = - compareSemver(version, ">=", "15.5.16") && - compareSemver(version, "<", "16"); - - // v15.5.16–v15.x, v16.2.5+ - if (isV15WithNewArgs || compareSemver(version, ">=", "16.2.5")) { - return fetchInternalImage( - href, - // @ts-expect-error - It is supposed to be an IncomingMessage object, but only the headers are used. - { headers }, - {}, // res object is not necessary as it's not actually used. - maximumResponseBody, - handleRequest, - ); - } - - // <=v15.5.15, v16.0.0–v16.2.4 - // @ts-expect-error - fetchInternalImage signature has changed in Next.js 15.5.16 and 16.2.5 - return fetchInternalImage( - href, - { headers }, - {}, // res object is not necessary as it's not actually used. - handleRequest, - ); -} - //#override optimizeImage export async function optimizeImage( headers: APIGatewayProxyEventHeaders, @@ -109,14 +32,77 @@ export async function optimizeImage( const maximumResponseBody = nextConfig.images?.maximumResponseBody || 50_000_000; - const imageUpstream = isAbsolute - ? await callFetchExternalImage(href, maximumResponseBody) - : await callFetchInternalImage( + /** + * fetchExternalImage signature varies across Next.js versions: + * <=v15.5.9: fetchExternalImage(href) + * v15.5.10–v15.x: fetchExternalImage(href, maximumResponseBody) + * v16.0.0–v16.1.4: fetchExternalImage(href, dangerouslyAllowLocalIP) + * v16.1.5+: fetchExternalImage(href, dangerouslyAllowLocalIP, maximumResponseBody) + * + * https://github.com/vercel/next.js/blob/bfe2ab4/packages/next/src/server/image-optimizer.ts#L711 + */ + const callFetchExternalImage = () => { + const version = globalThis.nextVersion; + + // v16.1.5+ + if (compareSemver(version, ">=", "16.1.5")) { + return fetchExternalImage(href, false, maximumResponseBody); + } + + // v16.0.0–v16.1.4 + if (compareSemver(version, ">=", "16")) { + // @ts-expect-error - fetchExternalImage signature varies across Next.js versions + return fetchExternalImage(href, false); + } + + // v15.5.10–v15.x + if (compareSemver(version, ">=", "15.5.10")) { + // @ts-expect-error - fetchExternalImage signature varies across Next.js versions + return fetchExternalImage(href, maximumResponseBody); + } + + // <=v15.5.9 + // @ts-expect-error - fetchExternalImage signature varies across Next.js versions + return fetchExternalImage(href); + }; + + /** + * fetchInternalImage signature varies across Next.js versions: + * <=v15.5.15, v16.0.0–v16.2.4: fetchInternalImage(href, req, res, handleRequest) + * v15.5.16–v15.x, v16.2.5+: fetchInternalImage(href, req, res, maximumResponseBody, handleRequest) + */ + const callFetchInternalImage = () => { + const version = globalThis.nextVersion; + + const isV15WithNewArgs = + compareSemver(version, ">=", "15.5.16") && + compareSemver(version, "<", "16"); + + // v15.5.16–v15.x, v16.2.5+ + if (isV15WithNewArgs || compareSemver(version, ">=", "16.2.5")) { + return fetchInternalImage( href, - headers, + // @ts-expect-error - It is supposed to be an IncomingMessage object, but only the headers are used. + { headers }, + {}, // res object is not necessary as it's not actually used. maximumResponseBody, handleRequest, ); + } + + // <=v15.5.15, v16.0.0–v16.2.4 + // @ts-expect-error - fetchInternalImage signature has changed in Next.js 15.5.16 and 16.2.5 + return fetchInternalImage( + href, + { headers }, + {}, // res object is not necessary as it's not actually used. + handleRequest, + ); + }; + + const imageUpstream = isAbsolute + ? await callFetchExternalImage() + : await callFetchInternalImage(); const result = await imageOptimizer( imageUpstream,