From b11bd49239b42e6deb218e08a6603f8ad4a98ed6 Mon Sep 17 00:00:00 2001 From: Steven Date: Thu, 12 Aug 2021 20:31:51 -0400 Subject: [PATCH] Fix image optimization encoding url (#28045) - Fixes #27973 - Reverts #27671 The problem with PR #27671 is that it was encoding too often when it really only needed to solve the bug for `next build && next start` since `next dev` was already working. This PR uses the alternative solution mentioned here https://github.com/vercel/next.js/issues/27210#issuecomment-890305204 --- packages/next/server/image-optimizer.ts | 8 +- packages/next/server/next-server.ts | 9 +- .../image-component/unicode/next.config.js | 5 + .../image-component/unicode/pages/index.js | 33 ++++++ .../unicode/public/hello world.jpg | Bin 0 -> 1545 bytes .../public/\303\244\303\266\303\274.png" | Bin 0 -> 1545 bytes .../unicode/test/index.test.js | 96 ++++++++++++++++++ 7 files changed, 145 insertions(+), 6 deletions(-) create mode 100644 test/integration/image-component/unicode/next.config.js create mode 100644 test/integration/image-component/unicode/pages/index.js create mode 100644 test/integration/image-component/unicode/public/hello world.jpg create mode 100644 "test/integration/image-component/unicode/public/\303\244\303\266\303\274.png" create mode 100644 test/integration/image-component/unicode/test/index.test.js diff --git a/packages/next/server/image-optimizer.ts b/packages/next/server/image-optimizer.ts index 4ddb4478f6d0163..eabb4ee55a35cdd 100644 --- a/packages/next/server/image-optimizer.ts +++ b/packages/next/server/image-optimizer.ts @@ -69,22 +69,20 @@ export async function imageOptimizer( } const { headers } = req - const { url: decodedUrl, w, q } = parsedUrl.query + const { url, w, q } = parsedUrl.query const mimeType = getSupportedMimeType(MODERN_TYPES, headers.accept) let href: string - if (!decodedUrl) { + if (!url) { res.statusCode = 400 res.end('"url" parameter is required') return { finished: true } - } else if (Array.isArray(decodedUrl)) { + } else if (Array.isArray(url)) { res.statusCode = 400 res.end('"url" parameter cannot be an array') return { finished: true } } - const url = encodeURI(decodedUrl) - let isAbsolute: boolean if (url.startsWith('/')) { diff --git a/packages/next/server/next-server.ts b/packages/next/server/next-server.ts index d62b6ded0d4b5ea..f3d90464fb31839 100644 --- a/packages/next/server/next-server.ts +++ b/packages/next/server/next-server.ts @@ -1158,7 +1158,14 @@ export default class Server { pathParts.splice(0, basePathParts.length) } - const path = `/${pathParts.join('/')}` + let path = `/${pathParts.join('/')}` + + if (!publicFiles.has(path)) { + // In `next-dev-server.ts`, we ensure encoded paths match + // decoded paths on the filesystem. So we need do the + // opposite here: make sure decoded paths match encoded. + path = encodeURI(path) + } if (publicFiles.has(path)) { await this.serveStatic( diff --git a/test/integration/image-component/unicode/next.config.js b/test/integration/image-component/unicode/next.config.js new file mode 100644 index 000000000000000..eb554b46c10c064 --- /dev/null +++ b/test/integration/image-component/unicode/next.config.js @@ -0,0 +1,5 @@ +module.exports = { + images: { + domains: ['image-optimization-test.vercel.app'], + }, +} diff --git a/test/integration/image-component/unicode/pages/index.js b/test/integration/image-component/unicode/pages/index.js new file mode 100644 index 000000000000000..bdc4ea840f65b79 --- /dev/null +++ b/test/integration/image-component/unicode/pages/index.js @@ -0,0 +1,33 @@ +import React from 'react' +import Image from 'next/image' +import img from '../public/äöü.png' + +const Page = () => { + return ( +
+

Unicode Image URL

+ + + + + +
+ ) +} + +export default Page diff --git a/test/integration/image-component/unicode/public/hello world.jpg b/test/integration/image-component/unicode/public/hello world.jpg new file mode 100644 index 0000000000000000000000000000000000000000..e14fafc5cf3bc63b70914ad20467f40f7fecd572 GIT binary patch literal 1545 zcmbVM{Xf$Q9A6%~&O#i9S`$MamWNGTo{nv7c{rq_?QHVUW~E6OQi@{ZJcmZ|?7l@Q zzFgO>LgSLHn=t)6BZh%t7EPMfk1SL z1Y86JvZ4In(b7~asTB_EYLbzJ#fBxtCqp2a7u(A{gEak&&i%OE5K&=dA02(f0EgVb zDQO?EwAgXhbPx#1STW3~N_6+*i-&gO&5gIVD)qtd)=yh(VkE{hpxOq=E?Uo-)5z*x z!Au!iA$YiLAm+*0qggP>?VsKD-2i&HQxQ3+OqX*8S}wK5H8(1QM_f{Jya%lp;-fFQ z-RxdA9ea)1aI;`EXvn#9J~1_}n?bl%WsA3~x1yF~ZJY?F%5TY1f>Os{GDi>X>C?IS zC87Oo3ZX}KJ*U`mZ%63leZQDa&ij+|L2Ig&kv$8+G!kJ)!A>IpI0!SpvZ=R*dmxwE z_A02!zif^Xi?D&?&%f0Tzbc>bI(#PkQsao89{0s~R(I*hM>py`YIH=n8s(l<+!VhFb)fj#H;uE`npo7 zY;0_#QmGRY6Algzb}0{05Qr9vi1UjyHCq}CIyy~&Xo)lk4660;XBm=IbzH;Vwux!6 z@U`%Q<6`U_r^#vHXzMH%_g}z&^bvih;Naksl&3F)p7Kn#$+goa*xhsUD|t?H%CawT z>JQ8!^fPzDF6c8waZPU1$^P~{X*y_EN`KC=6nc}~iEX#>ud*u)-GT=qZK~K!#eMKri|K2@v zeX7|gqiZ-a27vkY(m>jlb*A45J^WhNqUd5svx=i!WlyGoDxyIkDCJw8 zl1RKs=y0j+xtSIh@AZ-SU-~z%d7|iJXK0I}nj!QZ_;_V0t%N>WpH)B+RT91Kkuhzx zSp{CL@O&X!puOb5enarY#IKV0$GfaZ<5QCF#q6Ih66Bl1Pk?cT!sCl5^YK4KUf8=r z`aO#WUfA<6@Z|tBgFYm!h8b-eKV4c&$3bTW&<9YGGZ&`xG#9~EHI4;**~o$2bOc^F z)xqxjhTZjF)wtZ04Ns<6mIBW?61;SKUp&Ix#QrYF;SY_@rCeH2X2*tJ$*pAIHb zh#ej+0ZbcVCs7JzV7TsL6Jyyhc?vBAKW|d~E=#`(Epz?bhZI(;xeQ`sbe2CXvFp-!)9gAPmnDWWTsf>26XSP@ zv&2i`WrNZNf%ZoawxTiv7?Jj|6+NW@o>r`=449DMidcqyfhe1CUhQqXbvCSyC1#>! z&TQ9Zpp%MX zY5qJSn%bSF+=@PAVhp9?wWsW-al19&OZPE literal 0 HcmV?d00001 diff --git "a/test/integration/image-component/unicode/public/\303\244\303\266\303\274.png" "b/test/integration/image-component/unicode/public/\303\244\303\266\303\274.png" new file mode 100644 index 0000000000000000000000000000000000000000..e14fafc5cf3bc63b70914ad20467f40f7fecd572 GIT binary patch literal 1545 zcmbVM{Xf$Q9A6%~&O#i9S`$MamWNGTo{nv7c{rq_?QHVUW~E6OQi@{ZJcmZ|?7l@Q zzFgO>LgSLHn=t)6BZh%t7EPMfk1SL z1Y86JvZ4In(b7~asTB_EYLbzJ#fBxtCqp2a7u(A{gEak&&i%OE5K&=dA02(f0EgVb zDQO?EwAgXhbPx#1STW3~N_6+*i-&gO&5gIVD)qtd)=yh(VkE{hpxOq=E?Uo-)5z*x z!Au!iA$YiLAm+*0qggP>?VsKD-2i&HQxQ3+OqX*8S}wK5H8(1QM_f{Jya%lp;-fFQ z-RxdA9ea)1aI;`EXvn#9J~1_}n?bl%WsA3~x1yF~ZJY?F%5TY1f>Os{GDi>X>C?IS zC87Oo3ZX}KJ*U`mZ%63leZQDa&ij+|L2Ig&kv$8+G!kJ)!A>IpI0!SpvZ=R*dmxwE z_A02!zif^Xi?D&?&%f0Tzbc>bI(#PkQsao89{0s~R(I*hM>py`YIH=n8s(l<+!VhFb)fj#H;uE`npo7 zY;0_#QmGRY6Algzb}0{05Qr9vi1UjyHCq}CIyy~&Xo)lk4660;XBm=IbzH;Vwux!6 z@U`%Q<6`U_r^#vHXzMH%_g}z&^bvih;Naksl&3F)p7Kn#$+goa*xhsUD|t?H%CawT z>JQ8!^fPzDF6c8waZPU1$^P~{X*y_EN`KC=6nc}~iEX#>ud*u)-GT=qZK~K!#eMKri|K2@v zeX7|gqiZ-a27vkY(m>jlb*A45J^WhNqUd5svx=i!WlyGoDxyIkDCJw8 zl1RKs=y0j+xtSIh@AZ-SU-~z%d7|iJXK0I}nj!QZ_;_V0t%N>WpH)B+RT91Kkuhzx zSp{CL@O&X!puOb5enarY#IKV0$GfaZ<5QCF#q6Ih66Bl1Pk?cT!sCl5^YK4KUf8=r z`aO#WUfA<6@Z|tBgFYm!h8b-eKV4c&$3bTW&<9YGGZ&`xG#9~EHI4;**~o$2bOc^F z)xqxjhTZjF)wtZ04Ns<6mIBW?61;SKUp&Ix#QrYF;SY_@rCeH2X2*tJ$*pAIHb zh#ej+0ZbcVCs7JzV7TsL6Jyyhc?vBAKW|d~E=#`(Epz?bhZI(;xeQ`sbe2CXvFp-!)9gAPmnDWWTsf>26XSP@ zv&2i`WrNZNf%ZoawxTiv7?Jj|6+NW@o>r`=449DMidcqyfhe1CUhQqXbvCSyC1#>! z&TQ9Zpp%MX zY5qJSn%bSF+=@PAVhp9?wWsW-al19&OZPE literal 0 HcmV?d00001 diff --git a/test/integration/image-component/unicode/test/index.test.js b/test/integration/image-component/unicode/test/index.test.js new file mode 100644 index 000000000000000..53e224848dec500 --- /dev/null +++ b/test/integration/image-component/unicode/test/index.test.js @@ -0,0 +1,96 @@ +/* eslint-env jest */ + +import { + findPort, + killApp, + launchApp, + nextBuild, + nextStart, +} from 'next-test-utils' +import webdriver from 'next-webdriver' +//import fetch from 'node-fetch' +import { join } from 'path' + +jest.setTimeout(1000 * 60) + +const appDir = join(__dirname, '../') + +let appPort +let app +let browser + +function runTests() { + it('should load static unicode image', async () => { + const src = await browser.elementById('static').getAttribute('src') + expect(src).toMatch( + /_next%2Fstatic%2Fimage%2Fpublic%2F%C3%A4%C3%B6%C3%BC(.+)png/ + ) + const res = await fetch(src) + expect(res.status).toBe(200) + }) + + it('should load internal unicode image', async () => { + const src = await browser.elementById('internal').getAttribute('src') + expect(src).toMatch('/_next/image?url=%2F%C3%A4%C3%B6%C3%BC.png') + const res = await fetch(src) + expect(res.status).toBe(200) + }) + + it('should load external unicode image', async () => { + const src = await browser.elementById('external').getAttribute('src') + expect(src).toMatch( + '/_next/image?url=https%3A%2F%2Fimage-optimization-test.vercel.app%2F%C3%A4%C3%B6%C3%BC.png' + ) + const res = await fetch(src) + expect(res.status).toBe(200) + }) + + it('should load internal image with space', async () => { + const src = await browser.elementById('internal-space').getAttribute('src') + expect(src).toMatch('/_next/image?url=%2Fhello%2520world.jpg') + const res = await fetch(src) + expect(res.status).toBe(200) + }) + + it('should load external image with space', async () => { + const src = await browser.elementById('external-space').getAttribute('src') + expect(src).toMatch( + '/_next/image?url=https%3A%2F%2Fimage-optimization-test.vercel.app%2Fhello%2520world.jpg' + ) + const res = await fetch(src) + expect(res.status).toBe(200) + }) +} + +describe('Image Component Unicode Image URL', () => { + describe('dev mode', () => { + beforeAll(async () => { + appPort = await findPort() + app = await launchApp(appDir, appPort) + browser = await webdriver(appPort, '/') + }) + afterAll(() => { + killApp(app) + if (browser) { + browser.close() + } + }) + runTests() + }) + + describe('server mode', () => { + beforeAll(async () => { + await nextBuild(appDir) + appPort = await findPort() + app = await nextStart(appDir, appPort) + browser = await webdriver(appPort, '/') + }) + afterAll(() => { + killApp(app) + if (browser) { + browser.close() + } + }) + runTests() + }) +})