Skip to content

Commit b464d18

Browse files
authored
Update middleware public/static matching (#78875)
Backport of #78325 to 15.3.x
1 parent fa536cf commit b464d18

File tree

22 files changed

+300
-2
lines changed

22 files changed

+300
-2
lines changed

packages/next/src/server/lib/router-utils/resolve-routes.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,9 +461,23 @@ export function getResolveRoutes(
461461

462462
if (!opts.minimalMode && route.name === 'middleware') {
463463
const match = fsChecker.getMiddlewareMatchers()
464+
let maybeDecodedPathname = parsedUrl.pathname || '/'
465+
466+
try {
467+
maybeDecodedPathname = decodeURIComponent(maybeDecodedPathname)
468+
} catch {
469+
/* non-fatal we can't decode so can't match it */
470+
}
471+
464472
if (
465473
// @ts-expect-error BaseNextRequest stuff
466-
match?.(parsedUrl.pathname, req, parsedUrl.query)
474+
match?.(parsedUrl.pathname, req, parsedUrl.query) ||
475+
match?.(
476+
maybeDecodedPathname,
477+
// @ts-expect-error BaseNextRequest stuff
478+
req,
479+
parsedUrl.query
480+
)
467481
) {
468482
if (ensureMiddleware) {
469483
await ensureMiddleware(req.url)

packages/next/src/server/next-server.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1688,7 +1688,20 @@ export default class NextNodeServer extends BaseServer<
16881688

16891689
parsedUrl.pathname = pathnameInfo.pathname
16901690
const normalizedPathname = removeTrailingSlash(parsed.pathname || '')
1691-
if (!middleware.match(normalizedPathname, req, parsedUrl.query)) {
1691+
let maybeDecodedPathname = normalizedPathname
1692+
1693+
try {
1694+
maybeDecodedPathname = decodeURIComponent(normalizedPathname)
1695+
} catch {
1696+
/* non-fatal we can't decode so can't match it */
1697+
}
1698+
1699+
if (
1700+
!(
1701+
middleware.match(normalizedPathname, req, parsedUrl.query) ||
1702+
middleware.match(maybeDecodedPathname, req, parsedUrl.query)
1703+
)
1704+
) {
16921705
return handleFinished()
16931706
}
16941707

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <p>hello world</p>
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <p>hello world</p>
3+
}
Binary file not shown.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default function Page() {
2+
return <p>hello world</p>
3+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { Metadata } from 'next'
2+
3+
export const metadata: Metadata = {
4+
title: 'Create Next App',
5+
description: 'Generated by create next app',
6+
}
7+
8+
export default function RootLayout({
9+
children,
10+
}: Readonly<{
11+
children: React.ReactNode
12+
}>) {
13+
return (
14+
<html lang="en">
15+
<body>{children}</body>
16+
</html>
17+
)
18+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import Image from 'next/image'
2+
3+
export default function Home() {
4+
return (
5+
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
6+
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
7+
<Image
8+
className="dark:invert"
9+
src="/next.svg"
10+
alt="Next.js logo"
11+
width={180}
12+
height={38}
13+
priority
14+
/>
15+
<ol className="list-inside list-decimal text-sm/6 text-center sm:text-left font-[family-name:var(--font-geist-mono)]">
16+
<li className="mb-2 tracking-[-.01em]">
17+
Get started by editing{' '}
18+
<code className="bg-black/[.05] dark:bg-white/[.06] px-1 py-0.5 rounded font-[family-name:var(--font-geist-mono)] font-semibold">
19+
app/page.tsx
20+
</code>
21+
.
22+
</li>
23+
<li className="tracking-[-.01em]">
24+
Save and see your changes instantly.
25+
</li>
26+
</ol>
27+
28+
<div className="flex gap-4 items-center flex-col sm:flex-row">
29+
<a
30+
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
31+
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
32+
target="_blank"
33+
rel="noopener noreferrer"
34+
>
35+
<Image
36+
className="dark:invert"
37+
src="/vercel.svg"
38+
alt="Vercel logomark"
39+
width={20}
40+
height={20}
41+
/>
42+
Deploy now
43+
</a>
44+
<a
45+
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
46+
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
47+
target="_blank"
48+
rel="noopener noreferrer"
49+
>
50+
Read our docs
51+
</a>
52+
</div>
53+
</main>
54+
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
55+
<a
56+
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
57+
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
58+
target="_blank"
59+
rel="noopener noreferrer"
60+
>
61+
<Image
62+
aria-hidden
63+
src="/file.svg"
64+
alt="File icon"
65+
width={16}
66+
height={16}
67+
/>
68+
Learn
69+
</a>
70+
<a
71+
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
72+
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
73+
target="_blank"
74+
rel="noopener noreferrer"
75+
>
76+
<Image
77+
aria-hidden
78+
src="/window.svg"
79+
alt="Window icon"
80+
width={16}
81+
height={16}
82+
/>
83+
Examples
84+
</a>
85+
<a
86+
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
87+
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
88+
target="_blank"
89+
rel="noopener noreferrer"
90+
>
91+
<Image
92+
aria-hidden
93+
src="/globe.svg"
94+
alt="Globe icon"
95+
width={16}
96+
height={16}
97+
/>
98+
Go to nextjs.org →
99+
</a>
100+
</footer>
101+
</div>
102+
)
103+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { NextResponse } from 'next/server'
2+
3+
export const config = {
4+
matcher: [
5+
'/file.svg',
6+
'/vercel copy.svg',
7+
'/another/file.svg',
8+
'/another/hello',
9+
'/dynamic/:path*',
10+
'/glob/:path*',
11+
'/pages-another/hello',
12+
'/pages-dynamic/:path*',
13+
'/pages-glob/:path*',
14+
'/_next/static/css/:path*',
15+
],
16+
}
17+
18+
export default function middleware() {
19+
return NextResponse.json({ middleware: true })
20+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react'
2+
3+
export default function Page() {
4+
return (
5+
<>
6+
<p>pages-another/hello</p>
7+
</>
8+
)
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import React from 'react'
2+
3+
export default function Page() {
4+
return (
5+
<>
6+
<p>pages-another/hello</p>
7+
</>
8+
)
9+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.text {
2+
color: orange;
3+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import React from 'react'
2+
import * as styles from './hello.module.css'
3+
4+
export default function Page() {
5+
return (
6+
<>
7+
<p className={styles['text']}>pages-another/hello</p>
8+
</>
9+
)
10+
}
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/* eslint-env jest */
2+
3+
import glob from 'glob'
4+
import { join } from 'path'
5+
import { createNext, FileRef } from 'e2e-utils'
6+
import { isNextStart, NextInstance } from 'e2e-utils'
7+
8+
describe('Middleware Runtime', () => {
9+
let next: NextInstance
10+
let testPaths: Array<{ testPath: string }> = [
11+
{ testPath: '/file.svg' },
12+
{ testPath: '/vercel copy.svg' },
13+
{ testPath: '/vercel%20copy.svg' },
14+
{ testPath: '/another%2ffile.svg' },
15+
{ testPath: '/another/file.svg' },
16+
{ testPath: '/another/hello' },
17+
{ testPath: '/another%2fhello' },
18+
{ testPath: '/glob%2ffile.svg' },
19+
{ testPath: '/glob/file.svg' },
20+
{ testPath: '/dynamic%2f/first' },
21+
{ testPath: '/dynamic/first' },
22+
{ testPath: '/glob%2fhello' },
23+
{ testPath: '/glob/hello' },
24+
{ testPath: '/pages-another/hello' },
25+
{ testPath: '/pages-another%2fhello' },
26+
{ testPath: '/pages-dynamic%2f/first' },
27+
{ testPath: '/pages-dynamic/first' },
28+
{ testPath: '/pages-glob%2fhello' },
29+
{ testPath: '/pages-glob/hello' },
30+
]
31+
32+
beforeAll(async () => {
33+
next = await createNext({
34+
files: new FileRef(join(__dirname, 'app')),
35+
})
36+
})
37+
afterAll(async () => {
38+
await next.destroy()
39+
})
40+
41+
it.each(testPaths)(
42+
'should match middleware correctly for $testPath',
43+
async ({ testPath }) => {
44+
const res = await next.fetch(testPath, {
45+
redirect: 'manual',
46+
})
47+
48+
if (res.status === 404) {
49+
expect(await res.text()).toContain('page could not be found')
50+
} else {
51+
expect(await res.json()).toEqual({ middleware: true })
52+
}
53+
}
54+
)
55+
56+
if (isNextStart && !process.env.IS_TURBOPACK_TEST) {
57+
it('should match middleware of _next/static', async () => {
58+
const cssChunks = glob.sync('*.css', {
59+
cwd: join(next.testDir, '.next', 'static', 'css'),
60+
})
61+
62+
if (cssChunks.length < 1) {
63+
throw new Error(`Failed to find CSS chunk`)
64+
}
65+
66+
for (const testPath of [
67+
`/_next/static/css%2f${cssChunks[0]}`,
68+
`/_next/static/css/${cssChunks[0]}`,
69+
]) {
70+
const res = await next.fetch(testPath, {
71+
redirect: 'manual',
72+
})
73+
74+
if (res.status === 404) {
75+
expect(await res.text()).toContain('page could not be found')
76+
} else {
77+
expect(await res.json()).toEqual({ middleware: true })
78+
}
79+
}
80+
})
81+
}
82+
})

0 commit comments

Comments
 (0)