rsnext/packages/next/server/server-route-utils.ts
Javi Velasco 2d5d43fb75
Refactor server routing (#37725)
This PR fixes an issue where we have a middleware that rewrites every single request to the same origin while having `i18n` configured. It would be something like: 

```typescript
import { NextResponse } from 'next/server'

export function middleware(req) {
  return NextResponse.rewrite(req.nextUrl)
}
```

In this case we are going to be adding always the `locale` at the beginning of the destination since it is a rewrite. This causes static assets to not match and the whole application to break. I believe this is a potential footgun so in this PR we are addressing the issue by removing the locale from pathname for those cases where we check against the filesystem (e.g. public folder).

To achieve this change, this PR introduces some preparation changes and then a refactor of the logic in the server router. After this refactor we are going to be relying on properties that can be defined in the `Route` to decide wether or not we should remove the `basePath`, `locale`, etc instead of checking which _type_ of route it is that we are matching.

Overall this simplifies quite a lot the server router. The way we are testing the mentioned issue is by adding a default rewrite in the rewrite tests middleware.
2022-06-16 21:43:01 +00:00

181 lines
5.1 KiB
TypeScript

/* eslint-disable no-redeclare */
import type {
Header,
Redirect,
Rewrite,
RouteType,
} from '../lib/load-custom-routes'
import type { Route } from './router'
import type { BaseNextRequest } from './base-http'
import type { ParsedUrlQuery } from 'querystring'
import { getRedirectStatus, modifyRouteRegex } from '../lib/load-custom-routes'
import { getPathMatch } from '../shared/lib/router/utils/path-match'
import {
compileNonPath,
prepareDestination,
} from '../shared/lib/router/utils/prepare-destination'
import { getRequestMeta } from './request-meta'
import { stringify as stringifyQs } from 'querystring'
import { format as formatUrl } from 'url'
import { normalizeRepeatedSlashes } from '../shared/lib/utils'
export function getCustomRoute(params: {
rule: Header
type: RouteType
restrictedRedirectPaths: string[]
}): Route & Header
export function getCustomRoute(params: {
rule: Rewrite
type: RouteType
restrictedRedirectPaths: string[]
}): Route & Rewrite
export function getCustomRoute(params: {
rule: Redirect
type: RouteType
restrictedRedirectPaths: string[]
}): Route & Redirect
export function getCustomRoute(params: {
rule: Rewrite | Redirect | Header
type: RouteType
restrictedRedirectPaths: string[]
}): (Route & Rewrite) | (Route & Header) | (Route & Rewrite) {
const { rule, type, restrictedRedirectPaths } = params
const match = getPathMatch(rule.source, {
strict: true,
removeUnnamedParams: true,
regexModifier: !(rule as any).internal
? (regex: string) =>
modifyRouteRegex(
regex,
type === 'redirect' ? restrictedRedirectPaths : undefined
)
: undefined,
})
return {
...rule,
type,
match,
name: type,
fn: async (_req, _res, _params, _parsedUrl) => ({ finished: false }),
}
}
export const createHeaderRoute = ({
rule,
restrictedRedirectPaths,
}: {
rule: Header
restrictedRedirectPaths: string[]
}): Route => {
const headerRoute = getCustomRoute({
type: 'header',
rule,
restrictedRedirectPaths,
})
return {
match: headerRoute.match,
matchesBasePath: true,
matchesLocale: true,
matchesLocaleAPIRoutes: true,
matchesTrailingSlash: true,
has: headerRoute.has,
type: headerRoute.type,
name: `${headerRoute.type} ${headerRoute.source} header route`,
fn: async (_req, res, params, _parsedUrl) => {
const hasParams = Object.keys(params).length > 0
for (const header of headerRoute.headers) {
let { key, value } = header
if (hasParams) {
key = compileNonPath(key, params)
value = compileNonPath(value, params)
}
res.setHeader(key, value)
}
return { finished: false }
},
}
}
export const createRedirectRoute = ({
rule,
restrictedRedirectPaths,
}: {
rule: Redirect
restrictedRedirectPaths: string[]
}): Route => {
const redirectRoute = getCustomRoute({
type: 'redirect',
rule,
restrictedRedirectPaths,
})
return {
internal: redirectRoute.internal,
type: redirectRoute.type,
match: redirectRoute.match,
matchesBasePath: true,
matchesLocale: redirectRoute.internal ? undefined : true,
matchesLocaleAPIRoutes: true,
matchesTrailingSlash: true,
has: redirectRoute.has,
statusCode: redirectRoute.statusCode,
name: `Redirect route ${redirectRoute.source}`,
fn: async (req, res, params, parsedUrl) => {
const { parsedDestination } = prepareDestination({
appendParamsToQuery: false,
destination: redirectRoute.destination,
params: params,
query: parsedUrl.query,
})
const { query } = parsedDestination
delete (parsedDestination as any).query
parsedDestination.search = stringifyQuery(req, query)
let updatedDestination = formatUrl(parsedDestination)
if (updatedDestination.startsWith('/')) {
updatedDestination = normalizeRepeatedSlashes(updatedDestination)
}
res
.redirect(updatedDestination, getRedirectStatus(redirectRoute))
.body(updatedDestination)
.send()
return {
finished: true,
}
},
}
}
// since initial query values are decoded by querystring.parse
// we need to re-encode them here but still allow passing through
// values from rewrites/redirects
export const stringifyQuery = (req: BaseNextRequest, query: ParsedUrlQuery) => {
const initialQuery = getRequestMeta(req, '__NEXT_INIT_QUERY') || {}
const initialQueryValues: Array<string | string[]> =
Object.values(initialQuery)
return stringifyQs(query, undefined, undefined, {
encodeURIComponent(value) {
if (
value in initialQuery ||
initialQueryValues.some((initialQueryVal: string | string[]) => {
// `value` always refers to a query value, even if it's nested in an array
return Array.isArray(initialQueryVal)
? initialQueryVal.includes(value)
: initialQueryVal === value
})
) {
// Encode keys and values from initial query
return encodeURIComponent(value)
}
return value
},
})
}