Skip to content

Commit 1b8ab4e

Browse files
committedNov 8, 2022
Apply middleware patches
1 parent 58dcf30 commit 1b8ab4e

19 files changed

+494
-59
lines changed
 

‎packages/next/build/webpack-config.ts

+8
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,12 @@ export function getDefineEnv({
155155
'process.env.__NEXT_OPTIMISTIC_CLIENT_CACHE': JSON.stringify(
156156
config.experimental.optimisticClientCache
157157
),
158+
'process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE': JSON.stringify(
159+
config.experimental.skipMiddlewareUrlNormalize
160+
),
161+
'process.env.__NEXT_ALLOW_MIDDLEWARE_RESPONSE_BODY': JSON.stringify(
162+
config.experimental.allowMiddlewareResponseBody
163+
),
158164
'process.env.__NEXT_CROSS_ORIGIN': JSON.stringify(config.crossOrigin),
159165
'process.browser': JSON.stringify(isClient),
160166
'process.env.__NEXT_TEST_MODE': JSON.stringify(
@@ -1779,6 +1785,8 @@ export default async function getBaseWebpackConfig(
17791785
new MiddlewarePlugin({
17801786
dev,
17811787
sriEnabled: !dev && !!config.experimental.sri?.algorithm,
1788+
allowMiddlewareResponseBody:
1789+
!!config.experimental.allowMiddlewareResponseBody,
17821790
}),
17831791
isClient &&
17841792
new BuildManifestPlugin({

‎packages/next/build/webpack/plugins/middleware-plugin.ts

+18-4
Original file line numberDiff line numberDiff line change
@@ -335,12 +335,14 @@ function getCodeAnalyzer(params: {
335335
dev: boolean
336336
compiler: webpack.Compiler
337337
compilation: webpack.Compilation
338+
allowMiddlewareResponseBody: boolean
338339
}) {
339340
return (parser: webpack.javascript.JavascriptParser) => {
340341
const {
341342
dev,
342343
compiler: { webpack: wp },
343344
compilation,
345+
allowMiddlewareResponseBody,
344346
} = params
345347
const { hooks } = parser
346348

@@ -557,8 +559,10 @@ Learn More: https://nextjs.org/docs/messages/node-module-in-edge-runtime`,
557559
.for(`${prefix}WebAssembly.instantiate`)
558560
.tap(NAME, handleWrapWasmInstantiateExpression)
559561
}
560-
hooks.new.for('Response').tap(NAME, handleNewResponseExpression)
561-
hooks.new.for('NextResponse').tap(NAME, handleNewResponseExpression)
562+
if (!allowMiddlewareResponseBody) {
563+
hooks.new.for('Response').tap(NAME, handleNewResponseExpression)
564+
hooks.new.for('NextResponse').tap(NAME, handleNewResponseExpression)
565+
}
562566
hooks.callMemberChain.for('process').tap(NAME, handleCallMemberChain)
563567
hooks.expressionMemberChain.for('process').tap(NAME, handleCallMemberChain)
564568
hooks.importCall.tap(NAME, handleImport)
@@ -797,10 +801,19 @@ function getExtractMetadata(params: {
797801
export default class MiddlewarePlugin {
798802
private readonly dev: boolean
799803
private readonly sriEnabled: boolean
800-
801-
constructor({ dev, sriEnabled }: { dev: boolean; sriEnabled: boolean }) {
804+
private readonly allowMiddlewareResponseBody: boolean
805+
constructor({
806+
dev,
807+
sriEnabled,
808+
allowMiddlewareResponseBody,
809+
}: {
810+
dev: boolean
811+
sriEnabled: boolean
812+
allowMiddlewareResponseBody: boolean
813+
}) {
802814
this.dev = dev
803815
this.sriEnabled = sriEnabled
816+
this.allowMiddlewareResponseBody = allowMiddlewareResponseBody
804817
}
805818

806819
public apply(compiler: webpack.Compiler) {
@@ -813,6 +826,7 @@ export default class MiddlewarePlugin {
813826
dev: this.dev,
814827
compiler,
815828
compilation,
829+
allowMiddlewareResponseBody: this.allowMiddlewareResponseBody,
816830
})
817831
hooks.parser.for('javascript/auto').tap(NAME, codeAnalyzer)
818832
hooks.parser.for('javascript/dynamic').tap(NAME, codeAnalyzer)

‎packages/next/lib/load-custom-routes.ts

+41-39
Original file line numberDiff line numberDiff line change
@@ -624,50 +624,52 @@ export default async function loadCustomRoutes(
624624
)
625625
}
626626

627-
if (config.trailingSlash) {
628-
redirects.unshift(
629-
{
630-
source: '/:file((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/]+\\.\\w+)/',
631-
destination: '/:file',
632-
permanent: true,
633-
locale: config.i18n ? false : undefined,
634-
internal: true,
635-
} as Redirect,
636-
{
637-
source: '/:notfile((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/\\.]+)',
638-
destination: '/:notfile/',
639-
permanent: true,
640-
locale: config.i18n ? false : undefined,
641-
internal: true,
642-
} as Redirect
643-
)
644-
if (config.basePath) {
645-
redirects.unshift({
646-
source: config.basePath,
647-
destination: config.basePath + '/',
648-
permanent: true,
649-
basePath: false,
650-
locale: config.i18n ? false : undefined,
651-
internal: true,
652-
} as Redirect)
653-
}
654-
} else {
655-
redirects.unshift({
656-
source: '/:path+/',
657-
destination: '/:path+',
658-
permanent: true,
659-
locale: config.i18n ? false : undefined,
660-
internal: true,
661-
} as Redirect)
662-
if (config.basePath) {
627+
if (!config.experimental?.skipTrailingSlashRedirect) {
628+
if (config.trailingSlash) {
629+
redirects.unshift(
630+
{
631+
source: '/:file((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/]+\\.\\w+)/',
632+
destination: '/:file',
633+
permanent: true,
634+
locale: config.i18n ? false : undefined,
635+
internal: true,
636+
} as Redirect,
637+
{
638+
source: '/:notfile((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/\\.]+)',
639+
destination: '/:notfile/',
640+
permanent: true,
641+
locale: config.i18n ? false : undefined,
642+
internal: true,
643+
} as Redirect
644+
)
645+
if (config.basePath) {
646+
redirects.unshift({
647+
source: config.basePath,
648+
destination: config.basePath + '/',
649+
permanent: true,
650+
basePath: false,
651+
locale: config.i18n ? false : undefined,
652+
internal: true,
653+
} as Redirect)
654+
}
655+
} else {
663656
redirects.unshift({
664-
source: config.basePath + '/',
665-
destination: config.basePath,
657+
source: '/:path+/',
658+
destination: '/:path+',
666659
permanent: true,
667-
basePath: false,
668660
locale: config.i18n ? false : undefined,
669661
internal: true,
670662
} as Redirect)
663+
if (config.basePath) {
664+
redirects.unshift({
665+
source: config.basePath + '/',
666+
destination: config.basePath,
667+
permanent: true,
668+
basePath: false,
669+
locale: config.i18n ? false : undefined,
670+
internal: true,
671+
} as Redirect)
672+
}
671673
}
672674
}
673675

‎packages/next/server/base-server.ts

+27
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,33 @@ export default abstract class Server<ServerOptions extends Options = Options> {
434434
parsedUrl?: NextUrlWithParsedQuery
435435
): Promise<void> {
436436
try {
437+
// ensure cookies set in middleware are merged and
438+
// not overridden by API routes/getServerSideProps
439+
const _res = (res as any).originalResponse || res
440+
const origSetHeader = _res.setHeader.bind(_res)
441+
442+
_res.setHeader = (name: string, val: string | string[]) => {
443+
if (name.toLowerCase() === 'set-cookie') {
444+
const middlewareValue = getRequestMeta(req, '_nextMiddlewareCookie')
445+
446+
if (
447+
!middlewareValue ||
448+
!Array.isArray(val) ||
449+
!val.every((item, idx) => item === middlewareValue[idx])
450+
) {
451+
val = [
452+
...(middlewareValue || []),
453+
...(typeof val === 'string'
454+
? [val]
455+
: Array.isArray(val)
456+
? val
457+
: []),
458+
]
459+
}
460+
}
461+
return origSetHeader(name, val)
462+
}
463+
437464
const urlParts = (req.url || '').split('?')
438465
const urlNoQuery = urlParts[0]
439466

‎packages/next/server/config-schema.ts

+9
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,9 @@ const configSchema = {
222222
adjustFontFallbacks: {
223223
type: 'boolean',
224224
},
225+
allowMiddlewareResponseBody: {
226+
type: 'boolean',
227+
},
225228
amp: {
226229
additionalProperties: false,
227230
properties: {
@@ -348,6 +351,12 @@ const configSchema = {
348351
sharedPool: {
349352
type: 'boolean',
350353
},
354+
skipMiddlewareUrlNormalize: {
355+
type: 'boolean',
356+
},
357+
skipTrailingSlashRedirect: {
358+
type: 'boolean',
359+
},
351360
sri: {
352361
properties: {
353362
algorithm: {

‎packages/next/server/config-shared.ts

+3
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ export interface NextJsWebpackConfig {
7878
}
7979

8080
export interface ExperimentalConfig {
81+
allowMiddlewareResponseBody?: boolean
82+
skipMiddlewareUrlNormalize?: boolean
83+
skipTrailingSlashRedirect?: boolean
8184
optimisticClientCache?: boolean
8285
legacyBrowsers?: boolean
8386
browsersListForSwc?: boolean

‎packages/next/server/next-server.ts

+24-3
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'
8383
import { loadComponents } from './load-components'
8484
import isError, { getProperError } from '../lib/is-error'
8585
import { FontManifest } from './font-utils'
86-
import { toNodeHeaders } from './web/utils'
86+
import { splitCookiesString, toNodeHeaders } from './web/utils'
8787
import { relativizeURL } from '../shared/lib/router/utils/relativize-url'
8888
import { prepareDestination } from '../shared/lib/router/utils/prepare-destination'
8989
import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path'
@@ -1051,9 +1051,17 @@ export default class NextNodeServer extends BaseServer {
10511051
name: '_next/data catchall',
10521052
check: true,
10531053
fn: async (req, res, params, _parsedUrl) => {
1054+
const isNextDataNormalizing = getRequestMeta(
1055+
req,
1056+
'_nextDataNormalizing'
1057+
)
1058+
10541059
// Make sure to 404 for /_next/data/ itself and
10551060
// we also want to 404 if the buildId isn't correct
10561061
if (!params.path || params.path[0] !== this.buildId) {
1062+
if (isNextDataNormalizing) {
1063+
return { finished: false }
1064+
}
10571065
await this.render404(req, res, _parsedUrl)
10581066
return {
10591067
finished: true,
@@ -1797,6 +1805,14 @@ export default class NextNodeServer extends BaseServer {
17971805
} else {
17981806
for (let [key, value] of allHeaders) {
17991807
result.response.headers.set(key, value)
1808+
1809+
if (key.toLowerCase() === 'set-cookie') {
1810+
addRequestMeta(
1811+
params.request,
1812+
'_nextMiddlewareCookie',
1813+
splitCookiesString(value)
1814+
)
1815+
}
18001816
}
18011817
}
18021818

@@ -2105,8 +2121,13 @@ export default class NextNodeServer extends BaseServer {
21052121
params.res.statusCode = result.response.status
21062122
params.res.statusMessage = result.response.statusText
21072123

2108-
result.response.headers.forEach((value, key) => {
2109-
params.res.appendHeader(key, value)
2124+
result.response.headers.forEach((value: string, key) => {
2125+
// the append handling is special cased for `set-cookie`
2126+
if (key.toLowerCase() === 'set-cookie') {
2127+
params.res.setHeader(key, value)
2128+
} else {
2129+
params.res.appendHeader(key, value)
2130+
}
21102131
})
21112132

21122133
if (result.response.body) {

‎packages/next/server/request-meta.ts

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ export interface RequestMeta {
2222
_nextHadBasePath?: boolean
2323
_nextRewroteUrl?: string
2424
_protocol?: string
25+
_nextMiddlewareCookie?: string[]
26+
_nextDataNormalizing?: boolean
2527
}
2628

2729
export function getRequestMeta(

‎packages/next/server/router.ts

+18-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ import type {
77
} from '../shared/lib/router/utils/route-matcher'
88
import type { RouteHas } from '../lib/load-custom-routes'
99

10-
import { getNextInternalQuery, NextUrlWithParsedQuery } from './request-meta'
10+
import {
11+
addRequestMeta,
12+
getNextInternalQuery,
13+
NextUrlWithParsedQuery,
14+
} from './request-meta'
1115
import { getPathMatch } from '../shared/lib/router/utils/path-match'
1216
import { removeTrailingSlash } from '../shared/lib/router/utils/remove-trailing-slash'
1317
import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path'
@@ -189,7 +193,11 @@ export default class Router {
189193
...(middlewareCatchAllRoute
190194
? this.fsRoutes
191195
.filter((route) => route.name === '_next/data catchall')
192-
.map((route) => ({ ...route, check: false }))
196+
.map((route) => ({
197+
...route,
198+
name: '_next/data normalizing',
199+
check: false,
200+
}))
193201
: []),
194202
...this.headers,
195203
...this.redirects,
@@ -433,6 +441,11 @@ export default class Router {
433441
}
434442

435443
if (params) {
444+
const isNextDataNormalizing = route.name === '_next/data normalizing'
445+
446+
if (isNextDataNormalizing) {
447+
addRequestMeta(req, '_nextDataNormalizing', true)
448+
}
436449
parsedUrlUpdated.pathname = matchPathname
437450
const result = await route.fn(
438451
req,
@@ -441,6 +454,9 @@ export default class Router {
441454
parsedUrlUpdated,
442455
upgradeHead
443456
)
457+
if (isNextDataNormalizing) {
458+
addRequestMeta(req, '_nextDataNormalizing', false)
459+
}
444460
if (result.finished) {
445461
return true
446462
}

‎packages/next/server/web/adapter.ts

+18-10
Original file line numberDiff line numberDiff line change
@@ -110,11 +110,13 @@ export async function adapter(params: {
110110
nextConfig: params.request.nextConfig,
111111
})
112112

113-
if (rewriteUrl.host === request.nextUrl.host) {
114-
rewriteUrl.buildId = buildId || rewriteUrl.buildId
115-
rewriteUrl.flightSearchParameters =
116-
flightSearchParameters || rewriteUrl.flightSearchParameters
117-
response.headers.set('x-middleware-rewrite', String(rewriteUrl))
113+
if (!process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE) {
114+
if (rewriteUrl.host === request.nextUrl.host) {
115+
rewriteUrl.buildId = buildId || rewriteUrl.buildId
116+
rewriteUrl.flightSearchParameters =
117+
flightSearchParameters || rewriteUrl.flightSearchParameters
118+
response.headers.set('x-middleware-rewrite', String(rewriteUrl))
119+
}
118120
}
119121

120122
/**
@@ -149,11 +151,13 @@ export async function adapter(params: {
149151
*/
150152
response = new Response(response.body, response)
151153

152-
if (redirectURL.host === request.nextUrl.host) {
153-
redirectURL.buildId = buildId || redirectURL.buildId
154-
redirectURL.flightSearchParameters =
155-
flightSearchParameters || redirectURL.flightSearchParameters
156-
response.headers.set('Location', String(redirectURL))
154+
if (!process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE) {
155+
if (redirectURL.host === request.nextUrl.host) {
156+
redirectURL.buildId = buildId || redirectURL.buildId
157+
redirectURL.flightSearchParameters =
158+
flightSearchParameters || redirectURL.flightSearchParameters
159+
response.headers.set('Location', String(redirectURL))
160+
}
157161
}
158162

159163
/**
@@ -179,6 +183,10 @@ export async function adapter(params: {
179183
export function blockUnallowedResponse(
180184
promise: Promise<FetchEventResult>
181185
): Promise<FetchEventResult> {
186+
if (process.env.__NEXT_ALLOW_MIDDLEWARE_RESPONSE_BODY) {
187+
return promise
188+
}
189+
182190
return promise.then((result) => {
183191
if (result.response?.body) {
184192
console.error(

‎packages/next/server/web/next-url.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ export class NextURL {
100100
private analyzeUrl() {
101101
const pathnameInfo = getNextPathnameInfo(this[Internal].url.pathname, {
102102
nextConfig: this[Internal].options.nextConfig,
103-
parseData: true,
103+
parseData: !process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE,
104104
})
105105

106106
this[Internal].domainLocale = detectDomainLocale(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { NextResponse } from 'next/server'
2+
3+
export default function handler(req) {
4+
if (req.nextUrl.pathname.startsWith('/_next/data/missing-id')) {
5+
console.log(`missing-id rewrite: ${req.nextUrl.toString()}`)
6+
return NextResponse.rewrite('https://example.vercel.sh')
7+
}
8+
9+
if (req.nextUrl.pathname === '/middleware-rewrite-with-slash') {
10+
return NextResponse.rewrite(new URL('/another/', req.nextUrl))
11+
}
12+
13+
if (req.nextUrl.pathname === '/middleware-rewrite-without-slash') {
14+
return NextResponse.rewrite(new URL('/another', req.nextUrl))
15+
}
16+
17+
if (req.nextUrl.pathname === '/middleware-redirect-external-with') {
18+
return NextResponse.redirect('https://example.vercel.sh/somewhere/', 307)
19+
}
20+
21+
if (req.nextUrl.pathname === '/middleware-redirect-external-without') {
22+
return NextResponse.redirect('https://example.vercel.sh/somewhere', 307)
23+
}
24+
25+
if (req.nextUrl.pathname.startsWith('/api/test-cookie')) {
26+
const res = NextResponse.next()
27+
res.cookies.set('from-middleware', 1)
28+
return res
29+
}
30+
31+
if (req.nextUrl.pathname === '/middleware-response-body') {
32+
return new Response('hello from middleware', {
33+
headers: {
34+
'x-from-middleware': 'true',
35+
},
36+
})
37+
}
38+
39+
return NextResponse.next()
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/** @type {import('next').NextConfig} */
2+
const nextConfig = {
3+
experimental: {
4+
skipTrailingSlashRedirect: true,
5+
skipMiddlewareUrlNormalize: true,
6+
allowMiddlewareResponseBody: true,
7+
newNextLinkBehavior: true,
8+
},
9+
async redirects() {
10+
return [
11+
{
12+
source: '/redirect-me',
13+
destination: '/another',
14+
permanent: false,
15+
},
16+
]
17+
},
18+
async rewrites() {
19+
return [
20+
{
21+
source: '/rewrite-me',
22+
destination: '/another',
23+
},
24+
]
25+
},
26+
}
27+
28+
module.exports = nextConfig
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Link from 'next/link'
2+
3+
export default function Page(props) {
4+
return (
5+
<>
6+
<p id="another">another page</p>
7+
<Link href="/" id="to-index">
8+
to index
9+
</Link>
10+
<br />
11+
</>
12+
)
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { NextResponse } from 'next/server'
2+
3+
export const config = {
4+
runtime: 'experimental-edge',
5+
}
6+
7+
export default function handler(req) {
8+
console.log('setting cookie in api route')
9+
const res = NextResponse.json({ name: 'API' })
10+
res.cookies.set('hello', 'From API')
11+
return res
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default function handler(req, res) {
2+
console.log('setting cookie in api route')
3+
res.setHeader('Set-Cookie', 'hello=From API')
4+
res.status(200).json({ name: 'API' })
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Link from 'next/link'
2+
3+
export default function Page(props) {
4+
return (
5+
<>
6+
<p id="blog">blog page</p>
7+
<Link href="/" id="to-index">
8+
to index
9+
</Link>
10+
<br />
11+
</>
12+
)
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Link from 'next/link'
2+
3+
export default function Page(props) {
4+
return (
5+
<>
6+
<p id="index">index page</p>
7+
<Link href="/another" id="to-another">
8+
to another
9+
</Link>
10+
<br />
11+
<Link href="/blog/first" id="to-blog-first">
12+
to /blog/first
13+
</Link>
14+
<br />
15+
</>
16+
)
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { createNext, FileRef } from 'e2e-utils'
2+
import { NextInstance } from 'test/lib/next-modes/base'
3+
import { check, fetchViaHTTP } from 'next-test-utils'
4+
import { join } from 'path'
5+
import webdriver from 'next-webdriver'
6+
7+
describe('skip-trailing-slash-redirect', () => {
8+
let next: NextInstance
9+
10+
beforeAll(async () => {
11+
next = await createNext({
12+
files: new FileRef(join(__dirname, 'app')),
13+
dependencies: {},
14+
})
15+
})
16+
afterAll(() => next.destroy())
17+
18+
it('should allow rewriting invalid buildId correctly', async () => {
19+
const res = await fetchViaHTTP(
20+
next.url,
21+
'/_next/data/missing-id/hello.json',
22+
undefined,
23+
{
24+
headers: {
25+
'x-nextjs-data': '1',
26+
},
27+
}
28+
)
29+
expect(res.status).toBe(200)
30+
expect(await res.text()).toContain('Example Domain')
31+
32+
if (!(global as any).isNextDeploy) {
33+
await check(() => next.cliOutput, /missing-id rewrite/)
34+
expect(next.cliOutput).toContain('/_next/data/missing-id/hello.json')
35+
}
36+
})
37+
38+
it('should allow response body from middleware with flag', async () => {
39+
const res = await fetchViaHTTP(next.url, '/middleware-response-body')
40+
expect(res.status).toBe(200)
41+
expect(res.headers.get('x-from-middleware')).toBe('true')
42+
expect(await res.text()).toBe('hello from middleware')
43+
})
44+
45+
it('should merge cookies from middleware and API routes correctly', async () => {
46+
const res = await fetchViaHTTP(next.url, '/api/test-cookie', undefined, {
47+
redirect: 'manual',
48+
})
49+
expect(res.status).toBe(200)
50+
expect(res.headers.get('set-cookie')).toEqual(
51+
'from-middleware=1; Path=/, hello=From API'
52+
)
53+
})
54+
55+
it('should merge cookies from middleware and edge API routes correctly', async () => {
56+
const res = await fetchViaHTTP(
57+
next.url,
58+
'/api/test-cookie-edge',
59+
undefined,
60+
{
61+
redirect: 'manual',
62+
}
63+
)
64+
expect(res.status).toBe(200)
65+
expect(res.headers.get('set-cookie')).toEqual(
66+
'from-middleware=1; Path=/, hello=From%20API; Path=/'
67+
)
68+
})
69+
70+
if ((global as any).isNextStart) {
71+
it('should not have trailing slash redirects in manifest', async () => {
72+
const routesManifest = JSON.parse(
73+
await next.readFile('.next/routes-manifest.json')
74+
)
75+
76+
expect(
77+
routesManifest.redirects.some((redirect) => {
78+
return (
79+
redirect.statusCode === 308 &&
80+
(redirect.destination === '/:path+' ||
81+
redirect.destination === '/:path+/')
82+
)
83+
})
84+
).toBe(false)
85+
})
86+
}
87+
88+
it('should correct skip URL normalizing in middleware', async () => {
89+
let res = await fetchViaHTTP(
90+
next.url,
91+
'/middleware-rewrite-with-slash',
92+
undefined,
93+
{ redirect: 'manual', headers: { 'x-nextjs-data': '1' } }
94+
)
95+
expect(res.headers.get('x-nextjs-rewrite').endsWith('/another/')).toBe(true)
96+
97+
res = await fetchViaHTTP(
98+
next.url,
99+
'/middleware-rewrite-without-slash',
100+
undefined,
101+
{ redirect: 'manual', headers: { 'x-nextjs-data': '1' } }
102+
)
103+
expect(res.headers.get('x-nextjs-rewrite').endsWith('/another')).toBe(true)
104+
105+
res = await fetchViaHTTP(
106+
next.url,
107+
'/middleware-redirect-external-with',
108+
undefined,
109+
{ redirect: 'manual' }
110+
)
111+
expect(res.status).toBe(307)
112+
expect(res.headers.get('Location')).toBe(
113+
'https://example.vercel.sh/somewhere/'
114+
)
115+
116+
res = await fetchViaHTTP(
117+
next.url,
118+
'/middleware-redirect-external-without',
119+
undefined,
120+
{ redirect: 'manual' }
121+
)
122+
expect(res.status).toBe(307)
123+
expect(res.headers.get('Location')).toBe(
124+
'https://example.vercel.sh/somewhere'
125+
)
126+
})
127+
128+
it('should apply config redirect correctly', async () => {
129+
const res = await fetchViaHTTP(next.url, '/redirect-me', undefined, {
130+
redirect: 'manual',
131+
})
132+
expect(res.status).toBe(307)
133+
expect(new URL(res.headers.get('location'), 'http://n').pathname).toBe(
134+
'/another'
135+
)
136+
})
137+
138+
it('should apply config rewrites correctly', async () => {
139+
const res = await fetchViaHTTP(next.url, '/rewrite-me', undefined, {
140+
redirect: 'manual',
141+
})
142+
expect(res.status).toBe(200)
143+
expect(await res.text()).toContain('another page')
144+
})
145+
146+
it('should not apply trailing slash redirect (with slash)', async () => {
147+
const res = await fetchViaHTTP(next.url, '/another/', undefined, {
148+
redirect: 'manual',
149+
})
150+
expect(res.status).toBe(200)
151+
expect(await res.text()).toContain('another page')
152+
})
153+
154+
it('should not apply trailing slash redirect (without slash)', async () => {
155+
const res = await fetchViaHTTP(next.url, '/another', undefined, {
156+
redirect: 'manual',
157+
})
158+
expect(res.status).toBe(200)
159+
expect(await res.text()).toContain('another page')
160+
})
161+
162+
it('should respond to index correctly', async () => {
163+
const res = await fetchViaHTTP(next.url, '/', undefined, {
164+
redirect: 'manual',
165+
})
166+
expect(res.status).toBe(200)
167+
expect(await res.text()).toContain('index page')
168+
})
169+
170+
it('should respond to dynamic route correctly', async () => {
171+
const res = await fetchViaHTTP(next.url, '/blog/first', undefined, {
172+
redirect: 'manual',
173+
})
174+
expect(res.status).toBe(200)
175+
expect(await res.text()).toContain('blog page')
176+
})
177+
178+
it('should navigate client side correctly', async () => {
179+
const browser = await webdriver(next.url, '/')
180+
181+
expect(await browser.eval('location.pathname')).toBe('/')
182+
183+
await browser.elementByCss('#to-another').click()
184+
await browser.waitForElementByCss('#another')
185+
186+
expect(await browser.eval('location.pathname')).toBe('/another')
187+
await browser.back()
188+
await browser.waitForElementByCss('#index')
189+
190+
expect(await browser.eval('location.pathname')).toBe('/')
191+
192+
await browser.elementByCss('#to-blog-first').click()
193+
await browser.waitForElementByCss('#blog')
194+
195+
expect(await browser.eval('location.pathname')).toBe('/blog/first')
196+
})
197+
})

0 commit comments

Comments
 (0)
Please sign in to comment.