Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apply experimental configs for middleware #41142

Merged
merged 4 commits into from Oct 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/next/build/webpack-config.ts
Expand Up @@ -253,6 +253,9 @@ export function getDefineEnv({
'process.env.__NEXT_I18N_SUPPORT': JSON.stringify(!!config.i18n),
'process.env.__NEXT_I18N_DOMAINS': JSON.stringify(config.i18n?.domains),
'process.env.__NEXT_ANALYTICS_ID': JSON.stringify(config.analyticsId),
'process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE': JSON.stringify(
config.experimental.skipMiddlewareUrlNormalize
),
'process.env.__NEXT_HAS_WEB_VITALS_ATTRIBUTION': JSON.stringify(
config.experimental.webVitalsAttribution &&
config.experimental.webVitalsAttribution.length > 0
Expand Down
80 changes: 41 additions & 39 deletions packages/next/lib/load-custom-routes.ts
Expand Up @@ -624,50 +624,52 @@ export default async function loadCustomRoutes(
)
}

if (config.trailingSlash) {
redirects.unshift(
{
source: '/:file((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/]+\\.\\w+)/',
destination: '/:file',
permanent: true,
locale: config.i18n ? false : undefined,
internal: true,
} as Redirect,
{
source: '/:notfile((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/\\.]+)',
destination: '/:notfile/',
permanent: true,
locale: config.i18n ? false : undefined,
internal: true,
} as Redirect
)
if (config.basePath) {
redirects.unshift({
source: config.basePath,
destination: config.basePath + '/',
permanent: true,
basePath: false,
locale: config.i18n ? false : undefined,
internal: true,
} as Redirect)
}
} else {
redirects.unshift({
source: '/:path+/',
destination: '/:path+',
permanent: true,
locale: config.i18n ? false : undefined,
internal: true,
} as Redirect)
if (config.basePath) {
if (!config.experimental?.skipTrailingSlashRedirect) {
if (config.trailingSlash) {
redirects.unshift(
{
source: '/:file((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/]+\\.\\w+)/',
destination: '/:file',
permanent: true,
locale: config.i18n ? false : undefined,
internal: true,
} as Redirect,
{
source: '/:notfile((?!\\.well-known(?:/.*)?)(?:[^/]+/)*[^/\\.]+)',
destination: '/:notfile/',
permanent: true,
locale: config.i18n ? false : undefined,
internal: true,
} as Redirect
)
if (config.basePath) {
redirects.unshift({
source: config.basePath,
destination: config.basePath + '/',
permanent: true,
basePath: false,
locale: config.i18n ? false : undefined,
internal: true,
} as Redirect)
}
} else {
redirects.unshift({
source: config.basePath + '/',
destination: config.basePath,
source: '/:path+/',
destination: '/:path+',
permanent: true,
basePath: false,
locale: config.i18n ? false : undefined,
internal: true,
} as Redirect)
if (config.basePath) {
redirects.unshift({
source: config.basePath + '/',
destination: config.basePath,
permanent: true,
basePath: false,
locale: config.i18n ? false : undefined,
internal: true,
} as Redirect)
}
}
}

Expand Down
27 changes: 27 additions & 0 deletions packages/next/server/base-server.ts
Expand Up @@ -440,6 +440,33 @@ export default abstract class Server<ServerOptions extends Options = Options> {
parsedUrl?: NextUrlWithParsedQuery
): Promise<void> {
try {
// ensure cookies set in middleware are merged and
// not overridden by API routes/getServerSideProps
const _res = (res as any).originalResponse || res
const origSetHeader = _res.setHeader.bind(_res)

_res.setHeader = (name: string, val: string | string[]) => {
if (name.toLowerCase() === 'set-cookie') {
const middlewareValue = getRequestMeta(req, '_nextMiddlewareCookie')

if (
!middlewareValue ||
!Array.isArray(val) ||
!val.every((item, idx) => item === middlewareValue[idx])
) {
val = [
...(middlewareValue || []),
...(typeof val === 'string'
? [val]
: Array.isArray(val)
? val
: []),
]
}
}
return origSetHeader(name, val)
}

const urlParts = (req.url || '').split('?')
const urlNoQuery = urlParts[0]

Expand Down
6 changes: 6 additions & 0 deletions packages/next/server/config-schema.ts
Expand Up @@ -354,6 +354,12 @@ const configSchema = {
sharedPool: {
type: 'boolean',
},
skipMiddlewareUrlNormalize: {
type: 'boolean',
},
skipTrailingSlashRedirect: {
type: 'boolean',
},
sri: {
properties: {
algorithm: {
Expand Down
2 changes: 2 additions & 0 deletions packages/next/server/config-shared.ts
Expand Up @@ -79,6 +79,8 @@ export interface NextJsWebpackConfig {
}

export interface ExperimentalConfig {
skipMiddlewareUrlNormalize?: boolean
skipTrailingSlashRedirect?: boolean
optimisticClientCache?: boolean
legacyBrowsers?: boolean
browsersListForSwc?: boolean
Expand Down
20 changes: 16 additions & 4 deletions packages/next/server/next-server.ts
Expand Up @@ -84,7 +84,7 @@ import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path'
import { loadComponents } from './load-components'
import isError, { getProperError } from '../lib/is-error'
import { FontManifest } from './font-utils'
import { toNodeHeaders } from './web/utils'
import { splitCookiesString, toNodeHeaders } from './web/utils'
import { relativizeURL } from '../shared/lib/router/utils/relativize-url'
import { prepareDestination } from '../shared/lib/router/utils/prepare-destination'
import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path'
Expand Down Expand Up @@ -1796,9 +1796,16 @@ export default class NextNodeServer extends BaseServer {
} else {
for (let [key, value] of allHeaders) {
result.response.headers.set(key, value)

if (key.toLowerCase() === 'set-cookie') {
addRequestMeta(
params.request,
'_nextMiddlewareCookie',
splitCookiesString(value)
)
}
}
}

return result
}

Expand Down Expand Up @@ -2097,8 +2104,13 @@ export default class NextNodeServer extends BaseServer {
params.res.statusCode = result.response.status
params.res.statusMessage = result.response.statusText

result.response.headers.forEach((value, key) => {
params.res.appendHeader(key, value)
result.response.headers.forEach((value: string, key) => {
// the append handling is special cased for `set-cookie`
if (key.toLowerCase() === 'set-cookie') {
params.res.setHeader(key, value)
} else {
params.res.appendHeader(key, value)
}
})

if (result.response.body) {
Expand Down
1 change: 1 addition & 0 deletions packages/next/server/request-meta.ts
Expand Up @@ -21,6 +21,7 @@ export interface RequestMeta {
_nextDidRewrite?: boolean
_nextHadBasePath?: boolean
_nextRewroteUrl?: string
_nextMiddlewareCookie?: string[]
_protocol?: string
}

Expand Down
16 changes: 10 additions & 6 deletions packages/next/server/web/adapter.ts
Expand Up @@ -117,9 +117,11 @@ export async function adapter(params: {
nextConfig: params.request.nextConfig,
})

if (rewriteUrl.host === request.nextUrl.host) {
rewriteUrl.buildId = buildId || rewriteUrl.buildId
response.headers.set('x-middleware-rewrite', String(rewriteUrl))
if (!process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE) {
if (rewriteUrl.host === request.nextUrl.host) {
rewriteUrl.buildId = buildId || rewriteUrl.buildId
response.headers.set('x-middleware-rewrite', String(rewriteUrl))
}
}

/**
Expand Down Expand Up @@ -154,9 +156,11 @@ export async function adapter(params: {
*/
response = new Response(response.body, response)

if (redirectURL.host === request.nextUrl.host) {
redirectURL.buildId = buildId || redirectURL.buildId
response.headers.set('Location', String(redirectURL))
if (!process.env.__NEXT_NO_MIDDLEWARE_URL_NORMALIZE) {
if (redirectURL.host === request.nextUrl.host) {
redirectURL.buildId = buildId || redirectURL.buildId
response.headers.set('Location', String(redirectURL))
}
}

/**
Expand Down
27 changes: 27 additions & 0 deletions test/e2e/skip-trailing-slash-redirect/app/middleware.js
@@ -0,0 +1,27 @@
import { NextResponse } from 'next/server'

export default function handler(req) {
if (req.nextUrl.pathname === '/middleware-rewrite-with-slash') {
return NextResponse.rewrite(new URL('/another/', req.nextUrl))
}

if (req.nextUrl.pathname === '/middleware-rewrite-without-slash') {
return NextResponse.rewrite(new URL('/another', req.nextUrl))
}

if (req.nextUrl.pathname === '/middleware-redirect-external-with') {
return NextResponse.redirect('https://example.vercel.sh/somewhere/', 307)
}

if (req.nextUrl.pathname === '/middleware-redirect-external-without') {
return NextResponse.redirect('https://example.vercel.sh/somewhere', 307)
}

if (req.nextUrl.pathname.startsWith('/api/test-cookie')) {
const res = NextResponse.next()
res.cookies.set('from-middleware', 1)
return res
}

return NextResponse.next()
}
13 changes: 13 additions & 0 deletions test/e2e/skip-trailing-slash-redirect/app/pages/another.js
@@ -0,0 +1,13 @@
import Link from 'next/link'

export default function Page(props) {
return (
<>
<p id="another">another page</p>
<Link href="/">
<a id="to-index">to index</a>
</Link>
<br />
</>
)
}
@@ -0,0 +1,12 @@
import { NextResponse } from 'next/server'

export const config = {
runtime: 'experimental-edge',
}

export default function handler(req) {
console.log('setting cookie in api route')
const res = NextResponse.json({ name: 'API' })
res.cookies.set('hello', 'From API')
return res
}
@@ -0,0 +1,5 @@
export default function handler(req, res) {
console.log('setting cookie in api route')
res.setHeader('Set-Cookie', 'hello=From API')
res.status(200).json({ name: 'API' })
}
13 changes: 13 additions & 0 deletions test/e2e/skip-trailing-slash-redirect/app/pages/blog/[slug].js
@@ -0,0 +1,13 @@
import Link from 'next/link'

export default function Page(props) {
return (
<>
<p id="blog">blog page</p>
<Link href="/">
<a id="to-index">to index</a>
</Link>
<br />
</>
)
}
17 changes: 17 additions & 0 deletions test/e2e/skip-trailing-slash-redirect/app/pages/index.js
@@ -0,0 +1,17 @@
import Link from 'next/link'

export default function Page(props) {
return (
<>
<p id="index">index page</p>
<Link href="/another">
<a id="to-another">to another</a>
</Link>
<br />
<Link href="/blog/first">
<a id="to-blog-first">to /blog/first</a>
</Link>
<br />
</>
)
}