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

Refactor i18n checks on request handling #27328

Merged
merged 5 commits into from
Jul 21, 2021
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) {

let hasValidParams = true

setLazyProp({ req: req as any }, 'cookies', getCookieParser(req))
setLazyProp({ req: req as any }, 'cookies', getCookieParser(req.headers))

const options = {
App,
Expand Down
14 changes: 7 additions & 7 deletions packages/next/server/api-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export async function apiResolver(
const externalResolver = config.api?.externalResolver || false

// Parsing of cookies
setLazyProp({ req: apiReq }, 'cookies', getCookieParser(req))
setLazyProp({ req: apiReq }, 'cookies', getCookieParser(req.headers))
// Parsing query string
apiReq.query = query
// Parsing preview data
Expand Down Expand Up @@ -185,14 +185,14 @@ function parseJson(str: string): object {
}

/**
* Parse cookies from `req` header
* Parse cookies from the `headers` of request
* @param req request object
*/
export function getCookieParser(
req: IncomingMessage
): () => NextApiRequestCookies {
export function getCookieParser(headers: {
[key: string]: undefined | string | string[]
}): () => NextApiRequestCookies {
return function parseCookie(): NextApiRequestCookies {
const header: undefined | string | string[] = req.headers.cookie
const header: undefined | string | string[] = headers.cookie

if (!header) {
return {}
Expand Down Expand Up @@ -321,7 +321,7 @@ export function tryGetPreviewData(
return (req as any)[SYMBOL_PREVIEW_DATA] as any
}

const getCookies = getCookieParser(req)
const getCookies = getCookieParser(req.headers)
let cookies: NextApiRequestCookies
try {
cookies = getCookies()
Expand Down
28 changes: 15 additions & 13 deletions packages/next/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,28 @@ import os from 'os'
import { Header, Redirect, Rewrite } from '../lib/load-custom-routes'
import { ImageConfig, imageConfigDefault } from './image-config'

export type DomainLocales = Array<{
http?: true
domain: string
locales?: string[]
defaultLocale: string
}>

type NoOptionals<T> = {
[P in keyof T]-?: T[P]
}

export type NextConfigComplete = NoOptionals<NextConfig>

export interface I18NConfig {
defaultLocale: string
domains?: DomainLocale[]
localeDetection?: false
locales: string[]
}

export interface DomainLocale {
defaultLocale: string
domain: string
http?: true
locales?: string[]
}

export type NextConfig = { [key: string]: any } & {
i18n?: {
locales: string[]
defaultLocale: string
domains?: DomainLocales
localeDetection?: false
} | null
i18n?: I18NConfig | null

headers?: () => Promise<Header[]>
rewrites?: () => Promise<
Expand Down
2 changes: 1 addition & 1 deletion packages/next/server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { loadWebpackHook } from './config-utils'
import { ImageConfig, imageConfigDefault, VALID_LOADERS } from './image-config'
import { loadEnvConfig } from '@next/env'

export { DomainLocales, NextConfig, normalizeConfig } from './config-shared'
export { DomainLocale, NextConfig, normalizeConfig } from './config-shared'

const targets = ['server', 'serverless', 'experimental-serverless-trace']

Expand Down
186 changes: 33 additions & 153 deletions packages/next/server/next-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import {
tryGetPreviewData,
__ApiPreviewProps,
} from './api-utils'
import { DomainLocales, isTargetLikeServerless, NextConfig } from './config'
import { DomainLocale, isTargetLikeServerless, NextConfig } from './config'
import pathMatch from '../shared/lib/router/utils/path-match'
import { recursiveReadDirSync } from './lib/recursive-readdir-sync'
import { loadComponents, LoadComponentsReturnType } from './load-components'
Expand Down Expand Up @@ -83,13 +83,10 @@ import { removePathTrailingSlash } from '../client/normalize-trailing-slash'
import getRouteFromAssetPath from '../shared/lib/router/utils/get-route-from-asset-path'
import { FontManifest } from './font-utils'
import { denormalizePagePath } from './denormalize-page-path'
import accept from '@hapi/accept'
import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path'
import { detectLocaleCookie } from '../shared/lib/i18n/detect-locale-cookie'
import * as Log from '../build/output/log'
import { imageOptimizer } from './image-optimizer'
import { detectDomainLocale } from '../shared/lib/i18n/detect-domain-locale'
import cookie from 'next/dist/compiled/cookie'
import escapePathDelimiters from '../shared/lib/router/utils/escape-path-delimiters'
import { getUtils } from '../build/webpack/loaders/next-serverless-loader/utils'
import { PreviewData } from 'next/types'
Expand All @@ -98,6 +95,7 @@ import ResponseCache, {
ResponseCacheValue,
} from './response-cache'
import { NextConfigComplete } from './config-shared'
import { parseNextUrl } from '../shared/lib/router/utils/parse-next-url'

const getCustomRouteMatcher = pathMatch(true)

Expand Down Expand Up @@ -175,7 +173,7 @@ export default class Server {
locale?: string
locales?: string[]
defaultLocale?: string
domainLocales?: DomainLocales
domainLocales?: DomainLocale[]
distDir: string
}
private compression?: Middleware
Expand Down Expand Up @@ -309,7 +307,7 @@ export default class Server {
res: ServerResponse,
parsedUrl?: UrlWithParsedQuery
): Promise<void> {
setLazyProp({ req: req as any }, 'cookies', getCookieParser(req))
setLazyProp({ req: req as any }, 'cookies', getCookieParser(req.headers))

// Parse url if parsedUrl not provided
if (!parsedUrl || typeof parsedUrl !== 'object') {
Expand All @@ -324,9 +322,13 @@ export default class Server {
}
;(req as any).__NEXT_INIT_QUERY = Object.assign({}, parsedUrl.query)

if (basePath && req.url?.startsWith(basePath)) {
// store original URL to allow checking if basePath was
// provided or not
const url = parseNextUrl({
headers: req.headers,
nextConfig: this.nextConfig,
url: req.url?.replace(/^\/+/, '/'),
})

if (url.basePath) {
;(req as any)._nextHadBasePath = true
req.url = req.url!.replace(basePath, '') || '/'
}
Expand Down Expand Up @@ -436,156 +438,34 @@ export default class Server {
}`
}

if (i18n) {
// get pathname from URL with basePath stripped for locale detection
let { pathname, ...parsed } = parseUrl(req.url || '/')
pathname = pathname || '/'

let defaultLocale = i18n.defaultLocale
let detectedLocale = detectLocaleCookie(req, i18n.locales)
let acceptPreferredLocale
try {
acceptPreferredLocale =
i18n.localeDetection !== false
? accept.language(req.headers['accept-language'], i18n.locales)
: detectedLocale
} catch (_) {
acceptPreferredLocale = detectedLocale
}
const { host } = req?.headers || {}
// remove port from host if present
const hostname = host?.split(':')[0].toLowerCase()

const detectedDomain = detectDomainLocale(i18n.domains, hostname)
if (detectedDomain) {
defaultLocale = detectedDomain.defaultLocale
detectedLocale = defaultLocale
;(req as any).__nextIsLocaleDomain = true
}

// if not domain specific locale use accept-language preferred
detectedLocale = detectedLocale || acceptPreferredLocale

let localeDomainRedirect: string | undefined
;(req as any).__nextHadTrailingSlash = pathname!.endsWith('/')

if (pathname === '/') {
;(req as any).__nextHadTrailingSlash = this.nextConfig.trailingSlash
}
const localePathResult = normalizeLocalePath(pathname!, i18n.locales)

if (localePathResult.detectedLocale) {
detectedLocale = localePathResult.detectedLocale
req.url = formatUrl({
...parsed,
pathname: localePathResult.pathname,
})
;(req as any).__nextStrippedLocale = true

if (
localePathResult.pathname === '/api' ||
localePathResult.pathname.startsWith('/api/')
) {
return this.render404(req, res, parsedUrl)
}
}

// If a detected locale is a domain specific locale and we aren't already
// on that domain and path prefix redirect to it to prevent duplicate
// content from multiple domains
if (detectedDomain && pathname === '/') {
const localeToCheck = acceptPreferredLocale
// const localeToCheck = localePathResult.detectedLocale
// ? detectedLocale
// : acceptPreferredLocale

const matchedDomain = detectDomainLocale(
i18n.domains,
undefined,
localeToCheck
)
;(req as any).__nextHadTrailingSlash = url.locale?.trailingSlash
if (url.locale?.domain) {
;(req as any).__nextIsLocaleDomain = true
}

if (
matchedDomain &&
(matchedDomain.domain !== detectedDomain.domain ||
localeToCheck !== matchedDomain.defaultLocale)
) {
localeDomainRedirect = `http${matchedDomain.http ? '' : 's'}://${
matchedDomain.domain
}/${
localeToCheck === matchedDomain.defaultLocale ? '' : localeToCheck
}`
}
if (url.locale?.path.detectedLocale) {
req.url = formatUrl(url)
;(req as any).__nextStrippedLocale = true
if (url.pathname === '/api' || url.pathname.startsWith('/api/')) {
return this.render404(req, res, parsedUrl)
}
}

const denormalizedPagePath = denormalizePagePath(pathname || '/')
const detectedDefaultLocale =
!detectedLocale ||
detectedLocale.toLowerCase() === defaultLocale.toLowerCase()
const shouldStripDefaultLocale = false
// detectedDefaultLocale &&
// denormalizedPagePath.toLowerCase() ===
// `/${i18n.defaultLocale.toLowerCase()}`

const shouldAddLocalePrefix =
!detectedDefaultLocale && denormalizedPagePath === '/'

detectedLocale = detectedLocale || i18n.defaultLocale

if (
i18n.localeDetection !== false &&
(localeDomainRedirect ||
shouldAddLocalePrefix ||
shouldStripDefaultLocale)
) {
// set the NEXT_LOCALE cookie when a user visits the default locale
// with the locale prefix so that they aren't redirected back to
// their accept-language preferred locale
if (
shouldStripDefaultLocale &&
acceptPreferredLocale !== defaultLocale
) {
const previous = res.getHeader('set-cookie')

res.setHeader('set-cookie', [
...(typeof previous === 'string'
? [previous]
: Array.isArray(previous)
? previous
: []),
cookie.serialize('NEXT_LOCALE', defaultLocale, {
httpOnly: true,
path: '/',
}),
])
}

res.setHeader(
'Location',
localeDomainRedirect
? localeDomainRedirect
: formatUrl({
// make sure to include any query values when redirecting
...parsed,
pathname: shouldStripDefaultLocale
? basePath || `/`
: `${basePath || ''}/${detectedLocale}`,
})
)
res.statusCode = TEMPORARY_REDIRECT_STATUS
res.end()
return
if (!this.minimalMode || !parsedUrl.query.__nextLocale) {
if (url?.locale?.locale) {
parsedUrl.query.__nextLocale = url.locale.locale
}
}

parsedUrl.query.__nextDefaultLocale =
detectedDomain?.defaultLocale || i18n.defaultLocale
if (url?.locale?.defaultLocale) {
parsedUrl.query.__nextDefaultLocale = url.locale.defaultLocale
}

if (!this.minimalMode || !parsedUrl.query.__nextLocale) {
parsedUrl.query.__nextLocale =
localePathResult.detectedLocale ||
detectedDomain?.defaultLocale ||
defaultLocale
}
if (url.locale?.redirect) {
res.setHeader('Location', url.locale.redirect)
res.statusCode = TEMPORARY_REDIRECT_STATUS
res.end()
return
}

res.statusCode = 200
Expand Down
8 changes: 4 additions & 4 deletions packages/next/server/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ import {
getRedirectStatus,
Redirect,
} from '../lib/load-custom-routes'
import { DomainLocales } from './config'
import type { DomainLocale } from './config'

function noRouter() {
const message =
Expand All @@ -79,7 +79,7 @@ class ServerRouter implements NextRouter {
isReady: boolean
locales?: string[]
defaultLocale?: string
domainLocales?: DomainLocales
domainLocales?: DomainLocale[]
isPreview: boolean
isLocaleDomain: boolean

Expand All @@ -93,7 +93,7 @@ class ServerRouter implements NextRouter {
locale?: string,
locales?: string[],
defaultLocale?: string,
domainLocales?: DomainLocales,
domainLocales?: DomainLocale[],
isPreview?: boolean,
isLocaleDomain?: boolean
) {
Expand Down Expand Up @@ -186,7 +186,7 @@ export type RenderOptsPartial = {
locale?: string
locales?: string[]
defaultLocale?: string
domainLocales?: DomainLocales
domainLocales?: DomainLocale[]
disableOptimizedLoading?: boolean
requireStaticHTML?: boolean
}
Expand Down
Loading