From a028236df05439a86148c189b5462e4e88bad070 Mon Sep 17 00:00:00 2001 From: Pradeep Balaji Date: Thu, 13 Aug 2026 12:00:03 +0800 Subject: [PATCH 1/3] fix(ipx): resolve prerender 404s for encoded filenames #1378 --- playground/app/providers.ts | 6 ++++++ playground/public/images/test+plus.png | Bin 0 -> 8862 bytes src/runtime/server/routes/_ipx.ts | 8 +++++++- test/e2e/__snapshots__/ipx.json5 | 2 ++ test/e2e/generate.test.ts | 2 ++ test/unit/bundle.test.ts | 2 +- 6 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 playground/public/images/test+plus.png diff --git a/playground/app/providers.ts b/playground/app/providers.ts index d80670711..0a05c307d 100644 --- a/playground/app/providers.ts +++ b/playground/app/providers.ts @@ -93,6 +93,12 @@ export const providers: Provider[] = [ height: 300, link: 'https://unsplash.com/@omidarmin?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText', }, + { + src: '/images/test+plus.png', + from: 'Filename with + (issue #1378)', + width: 300, + height: 300, + }, ], }, // Aliyun diff --git a/playground/public/images/test+plus.png b/playground/public/images/test+plus.png new file mode 100644 index 0000000000000000000000000000000000000000..235232eae26acae7f01e1a78ba3a16607b3ff22d GIT binary patch literal 8862 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 literal 0 HcmV?d00001 diff --git a/src/runtime/server/routes/_ipx.ts b/src/runtime/server/routes/_ipx.ts index defcd87cf..3add17f0d 100644 --- a/src/runtime/server/routes/_ipx.ts +++ b/src/runtime/server/routes/_ipx.ts @@ -31,11 +31,17 @@ export default lazyEventHandler(() => { const ipx = createIPX(ipxOptions) const nodeHandler = createIPXNodeHandler(ipx, { parseURL(url) { - const parsedURL = new URL(url) + const parsedURL = new URL(url, 'http://localhost') let pathname = parsedURL.pathname if (baseURL && (pathname === baseURL || pathname.startsWith(`${baseURL}/`))) { pathname = pathname.slice(baseURL.length) || '/' } + try { + pathname = decodeURI(pathname) + } + catch { + // Keep original pathname if decoding fails. + } return parseIPXURL(parsedURL.origin + pathname + parsedURL.search) }, }) diff --git a/test/e2e/__snapshots__/ipx.json5 b/test/e2e/__snapshots__/ipx.json5 index 9ed913a9d..19e37e8cb 100644 --- a/test/e2e/__snapshots__/ipx.json5 +++ b/test/e2e/__snapshots__/ipx.json5 @@ -5,6 +5,7 @@ "/_ipx/s_300x300/images/colors.jpg", "/_ipx/s_300x300/images/everest.jpg", "/_ipx/s_300x300/images/tacos.svg", + "/_ipx/s_300x300/images/test%2Bplus.png", "/_ipx/s_300x300/unsplash/photo-1606112219348-204d7d8b94ee", ], "sources": [ @@ -13,6 +14,7 @@ "/_ipx/s_300x300/images/colors-layer-config.jpg", "/_ipx/s_300x300/images/everest.jpg", "/_ipx/s_300x300/images/tacos.svg", + "/_ipx/s_300x300/images/test%2Bplus.png", "/_ipx/s_300x300/unsplash/photo-1606112219348-204d7d8b94ee", ], } \ No newline at end of file diff --git a/test/e2e/generate.test.ts b/test/e2e/generate.test.ts index 4bb300721..0ab1c139f 100644 --- a/test/e2e/generate.test.ts +++ b/test/e2e/generate.test.ts @@ -40,12 +40,14 @@ describe('ipx provider', () => { "_ipx/s_300x300/images/colors.jpg", "_ipx/s_300x300/images/everest.jpg", "_ipx/s_300x300/images/tacos.svg", + "_ipx/s_300x300/images/test%2Bplus.png", "_ipx/s_300x300/unsplash/photo-1606112219348-204d7d8b94ee", "_ipx/s_600x600/images/colors-layer-config.jpg", "_ipx/s_600x600/images/colors-layer.jpg", "_ipx/s_600x600/images/colors.jpg", "_ipx/s_600x600/images/everest.jpg", "_ipx/s_600x600/images/tacos.svg", + "_ipx/s_600x600/images/test%2Bplus.png", "_ipx/s_600x600/unsplash/photo-1606112219348-204d7d8b94ee", ] `) diff --git a/test/unit/bundle.test.ts b/test/unit/bundle.test.ts index fb3db6277..f47201b59 100644 --- a/test/unit/bundle.test.ts +++ b/test/unit/bundle.test.ts @@ -21,7 +21,7 @@ describe.skipIf(process.env.ECOSYSTEM_CI || isWindows)('nuxt image bundle size', image: { provider: 'ipx' }, }) - expect(roundToKilobytes(withImage.totalBytes - withoutImage.totalBytes)).toMatchInlineSnapshot(`"11.4k"`) + expect(roundToKilobytes(withImage.totalBytes - withoutImage.totalBytes)).toMatchInlineSnapshot(`"12.2k"`) }) }) From 215e6a31bd31900ff030eaded8e9c16186370a73 Mon Sep 17 00:00:00 2001 From: Pradeep Balaji Date: Thu, 13 Aug 2026 12:11:31 +0800 Subject: [PATCH 2/3] revert: bundle test update --- test/unit/bundle.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/bundle.test.ts b/test/unit/bundle.test.ts index f47201b59..fb3db6277 100644 --- a/test/unit/bundle.test.ts +++ b/test/unit/bundle.test.ts @@ -21,7 +21,7 @@ describe.skipIf(process.env.ECOSYSTEM_CI || isWindows)('nuxt image bundle size', image: { provider: 'ipx' }, }) - expect(roundToKilobytes(withImage.totalBytes - withoutImage.totalBytes)).toMatchInlineSnapshot(`"12.2k"`) + expect(roundToKilobytes(withImage.totalBytes - withoutImage.totalBytes)).toMatchInlineSnapshot(`"11.4k"`) }) }) From 505a9fac885a177d49aaa95823ebba2e3ff0f80a Mon Sep 17 00:00:00 2001 From: Pradeep Balaji Date: Thu, 13 Aug 2026 13:19:17 +0800 Subject: [PATCH 3/3] fix: rearrange images on e2e --- test/e2e/__snapshots__/ipx.json5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/e2e/__snapshots__/ipx.json5 b/test/e2e/__snapshots__/ipx.json5 index 19e37e8cb..b601e7220 100644 --- a/test/e2e/__snapshots__/ipx.json5 +++ b/test/e2e/__snapshots__/ipx.json5 @@ -14,7 +14,7 @@ "/_ipx/s_300x300/images/colors-layer-config.jpg", "/_ipx/s_300x300/images/everest.jpg", "/_ipx/s_300x300/images/tacos.svg", - "/_ipx/s_300x300/images/test%2Bplus.png", "/_ipx/s_300x300/unsplash/photo-1606112219348-204d7d8b94ee", + "/_ipx/s_300x300/images/test%2Bplus.png", ], } \ No newline at end of file