diff --git a/packages/next/next-server/server/next-server.ts b/packages/next/next-server/server/next-server.ts index 2fbe9e6dd8adeb5ba040a9cecfbbb7078807b86f..63c7e363d7fee002e61b4494da02a20eb71f378f 100644 --- a/packages/next/next-server/server/next-server.ts +++ b/packages/next/next-server/server/next-server.ts @@ -647,30 +647,35 @@ export default class Server { } protected generatePublicRoutes(): Route[] { - const routes: Route[] = [] - const publicFiles = recursiveReadDirSync(this.publicDir) - - publicFiles.forEach(path => { - const unixPath = path.replace(/\\/g, '/') - // Only include public files that will not replace a page path - // this should not occur now that we check this during build - if (!this.pagesManifest![unixPath]) { - routes.push({ - match: route(unixPath), - type: 'route', - name: 'public catchall', - fn: async (req, res, _params, parsedUrl) => { - const p = join(this.publicDir, unixPath) - await this.serveStatic(req, res, p, parsedUrl) + const publicFiles = new Set( + recursiveReadDirSync(this.publicDir).map(p => p.replace(/\\/g, '/')) + ) + + return [ + { + match: route('/:path*'), + name: 'public folder catchall', + fn: async (req, res, params, parsedUrl) => { + const path = `/${(params.path || []).join('/')}` + + if (publicFiles.has(path)) { + await this.serveStatic( + req, + res, + // we need to re-encode it since send decodes it + join(this.dir, 'public', encodeURIComponent(path)), + parsedUrl + ) return { finished: true, } - }, - }) - } - }) - - return routes + } + return { + finished: false, + } + }, + } as Route, + ] } protected getDynamicRoutes() { diff --git a/packages/next/server/next-dev-server.ts b/packages/next/server/next-dev-server.ts index 83e49e21f6381d8e05a0628e7904fe6069671313..b37f5e3a0cd21a333bf9d377757ac4d487671e67 100644 --- a/packages/next/server/next-dev-server.ts +++ b/packages/next/server/next-dev-server.ts @@ -258,14 +258,14 @@ export default class DevServer extends Server { protected async _beforeCatchAllRender( req: IncomingMessage, res: ServerResponse, - _params: Params, + params: Params, parsedUrl: UrlWithParsedQuery ) { const { pathname } = parsedUrl - + const path = `/${(params.path || []).join('/')}` // check for a public file, throwing error if there's a // conflicting page - if (await this.hasPublicFile(pathname!)) { + if (await this.hasPublicFile(path)) { if (await this.hasPage(pathname!)) { const err = new Error( `A conflicting public file and page file was found for path ${pathname} https://err.sh/zeit/next.js/conflicting-public-file-page` @@ -274,7 +274,7 @@ export default class DevServer extends Server { await this.renderError(err, req, res, pathname!, {}) return true } - await this.servePublic(req, res, pathname!) + await this.servePublic(req, res, path) return true } @@ -484,7 +484,8 @@ export default class DevServer extends Server { } servePublic(req: IncomingMessage, res: ServerResponse, path: string) { - const p = join(this.publicDir, path) + const p = join(this.publicDir, encodeURIComponent(path)) + // we need to re-encode it since send decodes it return this.serveStatic(req, res, p) } diff --git a/test/integration/dynamic-routing/public/hello copy.txt b/test/integration/dynamic-routing/public/hello copy.txt new file mode 100644 index 0000000000000000000000000000000000000000..48941bc083e7e77784f8798a972a4be8696f4158 --- /dev/null +++ b/test/integration/dynamic-routing/public/hello copy.txt @@ -0,0 +1 @@ +hello world copy \ No newline at end of file diff --git a/test/integration/dynamic-routing/public/hello%20copy.txt b/test/integration/dynamic-routing/public/hello%20copy.txt new file mode 100644 index 0000000000000000000000000000000000000000..c619e3b1f68fbf77a7643f03be0b61d9995a5a4e --- /dev/null +++ b/test/integration/dynamic-routing/public/hello%20copy.txt @@ -0,0 +1 @@ +hello world %20 \ No newline at end of file diff --git a/test/integration/dynamic-routing/public/hello+copy.txt b/test/integration/dynamic-routing/public/hello+copy.txt new file mode 100644 index 0000000000000000000000000000000000000000..2c6733445659c9696c5bb945f6d093718a6a3b6e --- /dev/null +++ b/test/integration/dynamic-routing/public/hello+copy.txt @@ -0,0 +1 @@ +hello world + \ No newline at end of file diff --git a/test/integration/dynamic-routing/test/index.test.js b/test/integration/dynamic-routing/test/index.test.js index 492639cbd899493db9c6b254ed02478ed73aae80..643c6407b82a3226f60823d8e8a214607fd8ae10 100644 --- a/test/integration/dynamic-routing/test/index.test.js +++ b/test/integration/dynamic-routing/test/index.test.js @@ -350,6 +350,34 @@ function runTests(dev) { expect(data).toMatch(/hello world/) }) + it('should serve file with space from public folder', async () => { + const res = await fetchViaHTTP(appPort, '/hello copy.txt') + const text = (await res.text()).trim() + expect(text).toBe('hello world copy') + expect(res.status).toBe(200) + }) + + it('should serve file with plus from public folder', async () => { + const res = await fetchViaHTTP(appPort, '/hello+copy.txt') + const text = (await res.text()).trim() + expect(text).toBe('hello world +') + expect(res.status).toBe(200) + }) + + it('should serve file from public folder encoded', async () => { + const res = await fetchViaHTTP(appPort, '/hello%20copy.txt') + const text = (await res.text()).trim() + expect(text).toBe('hello world copy') + expect(res.status).toBe(200) + }) + + it('should serve file with %20 from public folder', async () => { + const res = await fetchViaHTTP(appPort, '/hello%2520copy.txt') + const text = (await res.text()).trim() + expect(text).toBe('hello world %20') + expect(res.status).toBe(200) + }) + if (dev) { it('should work with HMR correctly', async () => { const browser = await webdriver(appPort, '/post-1/comments') @@ -389,6 +417,10 @@ function runTests(dev) { join(appDir, '.next/routes-manifest.json') ) + for (const route of manifest.dynamicRoutes) { + route.regex = normalizeRegEx(route.regex) + } + expect(manifest).toEqual({ version: 1, basePath: '',