diff --git a/packages/next/src/server/lib/router-utils/resolve-routes.ts b/packages/next/src/server/lib/router-utils/resolve-routes.ts index 8b62bd2880c98..9f35dea53f9ff 100644 --- a/packages/next/src/server/lib/router-utils/resolve-routes.ts +++ b/packages/next/src/server/lib/router-utils/resolve-routes.ts @@ -461,9 +461,23 @@ export function getResolveRoutes( if (!opts.minimalMode && route.name === 'middleware') { const match = fsChecker.getMiddlewareMatchers() + let maybeDecodedPathname = parsedUrl.pathname || '/' + + try { + maybeDecodedPathname = decodeURIComponent(maybeDecodedPathname) + } catch { + /* non-fatal we can't decode so can't match it */ + } + if ( // @ts-expect-error BaseNextRequest stuff - match?.(parsedUrl.pathname, req, parsedUrl.query) + match?.(parsedUrl.pathname, req, parsedUrl.query) || + match?.( + maybeDecodedPathname, + // @ts-expect-error BaseNextRequest stuff + req, + parsedUrl.query + ) ) { if (ensureMiddleware) { await ensureMiddleware(req.url) diff --git a/packages/next/src/server/next-server.ts b/packages/next/src/server/next-server.ts index eb98fd4bc565e..54a1b9ca16195 100644 --- a/packages/next/src/server/next-server.ts +++ b/packages/next/src/server/next-server.ts @@ -1688,7 +1688,20 @@ export default class NextNodeServer extends BaseServer< parsedUrl.pathname = pathnameInfo.pathname const normalizedPathname = removeTrailingSlash(parsed.pathname || '') - if (!middleware.match(normalizedPathname, req, parsedUrl.query)) { + let maybeDecodedPathname = normalizedPathname + + try { + maybeDecodedPathname = decodeURIComponent(normalizedPathname) + } catch { + /* non-fatal we can't decode so can't match it */ + } + + if ( + !( + middleware.match(normalizedPathname, req, parsedUrl.query) || + middleware.match(maybeDecodedPathname, req, parsedUrl.query) + ) + ) { return handleFinished() } diff --git a/test/e2e/middleware-static-files/app/app/another/hello/page.tsx b/test/e2e/middleware-static-files/app/app/another/hello/page.tsx new file mode 100644 index 0000000000000..ff7159d9149fe --- /dev/null +++ b/test/e2e/middleware-static-files/app/app/another/hello/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

hello world

+} diff --git a/test/e2e/middleware-static-files/app/app/dynamic/[slug]/page.tsx b/test/e2e/middleware-static-files/app/app/dynamic/[slug]/page.tsx new file mode 100644 index 0000000000000..ff7159d9149fe --- /dev/null +++ b/test/e2e/middleware-static-files/app/app/dynamic/[slug]/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

hello world

+} diff --git a/test/e2e/middleware-static-files/app/app/favicon.ico b/test/e2e/middleware-static-files/app/app/favicon.ico new file mode 100644 index 0000000000000..718d6fea4835e Binary files /dev/null and b/test/e2e/middleware-static-files/app/app/favicon.ico differ diff --git a/test/e2e/middleware-static-files/app/app/glob/hello/page.tsx b/test/e2e/middleware-static-files/app/app/glob/hello/page.tsx new file mode 100644 index 0000000000000..ff7159d9149fe --- /dev/null +++ b/test/e2e/middleware-static-files/app/app/glob/hello/page.tsx @@ -0,0 +1,3 @@ +export default function Page() { + return

hello world

+} diff --git a/test/e2e/middleware-static-files/app/app/layout.tsx b/test/e2e/middleware-static-files/app/app/layout.tsx new file mode 100644 index 0000000000000..6b8b4518030f1 --- /dev/null +++ b/test/e2e/middleware-static-files/app/app/layout.tsx @@ -0,0 +1,18 @@ +import type { Metadata } from 'next' + +export const metadata: Metadata = { + title: 'Create Next App', + description: 'Generated by create next app', +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + {children} + + ) +} diff --git a/test/e2e/middleware-static-files/app/app/page.tsx b/test/e2e/middleware-static-files/app/app/page.tsx new file mode 100644 index 0000000000000..7a5011548102a --- /dev/null +++ b/test/e2e/middleware-static-files/app/app/page.tsx @@ -0,0 +1,103 @@ +import Image from 'next/image' + +export default function Home() { + return ( +
+
+ Next.js logo +
    +
  1. + Get started by editing{' '} + + app/page.tsx + + . +
  2. +
  3. + Save and see your changes instantly. +
  4. +
+ +
+ + Vercel logomark + Deploy now + + + Read our docs + +
+
+ +
+ ) +} diff --git a/test/e2e/middleware-static-files/app/middleware.js b/test/e2e/middleware-static-files/app/middleware.js new file mode 100644 index 0000000000000..6a444b3795aec --- /dev/null +++ b/test/e2e/middleware-static-files/app/middleware.js @@ -0,0 +1,20 @@ +import { NextResponse } from 'next/server' + +export const config = { + matcher: [ + '/file.svg', + '/vercel copy.svg', + '/another/file.svg', + '/another/hello', + '/dynamic/:path*', + '/glob/:path*', + '/pages-another/hello', + '/pages-dynamic/:path*', + '/pages-glob/:path*', + '/_next/static/css/:path*', + ], +} + +export default function middleware() { + return NextResponse.json({ middleware: true }) +} diff --git a/test/e2e/middleware-static-files/app/pages/pages-another/hello.tsx b/test/e2e/middleware-static-files/app/pages/pages-another/hello.tsx new file mode 100644 index 0000000000000..11bce713475d7 --- /dev/null +++ b/test/e2e/middleware-static-files/app/pages/pages-another/hello.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +export default function Page() { + return ( + <> +

pages-another/hello

+ + ) +} diff --git a/test/e2e/middleware-static-files/app/pages/pages-dynamic/[slug].tsx b/test/e2e/middleware-static-files/app/pages/pages-dynamic/[slug].tsx new file mode 100644 index 0000000000000..11bce713475d7 --- /dev/null +++ b/test/e2e/middleware-static-files/app/pages/pages-dynamic/[slug].tsx @@ -0,0 +1,9 @@ +import React from 'react' + +export default function Page() { + return ( + <> +

pages-another/hello

+ + ) +} diff --git a/test/e2e/middleware-static-files/app/pages/pages-glob/hello.module.css b/test/e2e/middleware-static-files/app/pages/pages-glob/hello.module.css new file mode 100644 index 0000000000000..5cbb1546d08e2 --- /dev/null +++ b/test/e2e/middleware-static-files/app/pages/pages-glob/hello.module.css @@ -0,0 +1,3 @@ +.text { + color: orange; +} diff --git a/test/e2e/middleware-static-files/app/pages/pages-glob/hello.tsx b/test/e2e/middleware-static-files/app/pages/pages-glob/hello.tsx new file mode 100644 index 0000000000000..cf2fff96c90f7 --- /dev/null +++ b/test/e2e/middleware-static-files/app/pages/pages-glob/hello.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import * as styles from './hello.module.css' + +export default function Page() { + return ( + <> +

pages-another/hello

+ + ) +} diff --git a/test/e2e/middleware-static-files/app/public/another/file.svg b/test/e2e/middleware-static-files/app/public/another/file.svg new file mode 100644 index 0000000000000..004145cddf3f9 --- /dev/null +++ b/test/e2e/middleware-static-files/app/public/another/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/e2e/middleware-static-files/app/public/file.svg b/test/e2e/middleware-static-files/app/public/file.svg new file mode 100644 index 0000000000000..004145cddf3f9 --- /dev/null +++ b/test/e2e/middleware-static-files/app/public/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/e2e/middleware-static-files/app/public/glob/file.svg b/test/e2e/middleware-static-files/app/public/glob/file.svg new file mode 100644 index 0000000000000..004145cddf3f9 --- /dev/null +++ b/test/e2e/middleware-static-files/app/public/glob/file.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/e2e/middleware-static-files/app/public/globe.svg b/test/e2e/middleware-static-files/app/public/globe.svg new file mode 100644 index 0000000000000..567f17b0d7c7f --- /dev/null +++ b/test/e2e/middleware-static-files/app/public/globe.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/e2e/middleware-static-files/app/public/next.svg b/test/e2e/middleware-static-files/app/public/next.svg new file mode 100644 index 0000000000000..5174b28c565c2 --- /dev/null +++ b/test/e2e/middleware-static-files/app/public/next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/e2e/middleware-static-files/app/public/vercel copy.svg b/test/e2e/middleware-static-files/app/public/vercel copy.svg new file mode 100644 index 0000000000000..77053960334e2 --- /dev/null +++ b/test/e2e/middleware-static-files/app/public/vercel copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/e2e/middleware-static-files/app/public/vercel.svg b/test/e2e/middleware-static-files/app/public/vercel.svg new file mode 100644 index 0000000000000..77053960334e2 --- /dev/null +++ b/test/e2e/middleware-static-files/app/public/vercel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/e2e/middleware-static-files/app/public/window.svg b/test/e2e/middleware-static-files/app/public/window.svg new file mode 100644 index 0000000000000..b2b2a44f6ebc7 --- /dev/null +++ b/test/e2e/middleware-static-files/app/public/window.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/test/e2e/middleware-static-files/index.test.ts b/test/e2e/middleware-static-files/index.test.ts new file mode 100644 index 0000000000000..66c595822a835 --- /dev/null +++ b/test/e2e/middleware-static-files/index.test.ts @@ -0,0 +1,82 @@ +/* eslint-env jest */ + +import glob from 'glob' +import { join } from 'path' +import { createNext, FileRef } from 'e2e-utils' +import { isNextStart, NextInstance } from 'e2e-utils' + +describe('Middleware Runtime', () => { + let next: NextInstance + let testPaths: Array<{ testPath: string }> = [ + { testPath: '/file.svg' }, + { testPath: '/vercel copy.svg' }, + { testPath: '/vercel%20copy.svg' }, + { testPath: '/another%2ffile.svg' }, + { testPath: '/another/file.svg' }, + { testPath: '/another/hello' }, + { testPath: '/another%2fhello' }, + { testPath: '/glob%2ffile.svg' }, + { testPath: '/glob/file.svg' }, + { testPath: '/dynamic%2f/first' }, + { testPath: '/dynamic/first' }, + { testPath: '/glob%2fhello' }, + { testPath: '/glob/hello' }, + { testPath: '/pages-another/hello' }, + { testPath: '/pages-another%2fhello' }, + { testPath: '/pages-dynamic%2f/first' }, + { testPath: '/pages-dynamic/first' }, + { testPath: '/pages-glob%2fhello' }, + { testPath: '/pages-glob/hello' }, + ] + + beforeAll(async () => { + next = await createNext({ + files: new FileRef(join(__dirname, 'app')), + }) + }) + afterAll(async () => { + await next.destroy() + }) + + it.each(testPaths)( + 'should match middleware correctly for $testPath', + async ({ testPath }) => { + const res = await next.fetch(testPath, { + redirect: 'manual', + }) + + if (res.status === 404) { + expect(await res.text()).toContain('page could not be found') + } else { + expect(await res.json()).toEqual({ middleware: true }) + } + } + ) + + if (isNextStart && !process.env.IS_TURBOPACK_TEST) { + it('should match middleware of _next/static', async () => { + const cssChunks = glob.sync('*.css', { + cwd: join(next.testDir, '.next', 'static', 'css'), + }) + + if (cssChunks.length < 1) { + throw new Error(`Failed to find CSS chunk`) + } + + for (const testPath of [ + `/_next/static/css%2f${cssChunks[0]}`, + `/_next/static/css/${cssChunks[0]}`, + ]) { + const res = await next.fetch(testPath, { + redirect: 'manual', + }) + + if (res.status === 404) { + expect(await res.text()).toContain('page could not be found') + } else { + expect(await res.json()).toEqual({ middleware: true }) + } + } + }) + } +})