2021-12-05 22:53:11 +01:00
import type { __ApiPreviewProps } from './api-utils'
2022-01-26 07:22:11 +01:00
import type { CustomRoutes } from '../lib/load-custom-routes'
2021-12-05 22:53:11 +01:00
import type { DomainLocale } from './config'
import type { DynamicRoutes , PageChecker , Params , Route } from './router'
import type { FontManifest } from './font-utils'
import type { LoadComponentsReturnType } from './load-components'
import type { MiddlewareManifest } from '../build/webpack/plugins/middleware-plugin'
import type { NextConfig , NextConfigComplete } from './config-shared'
import type { NextParsedUrlQuery , NextUrlWithParsedQuery } from './request-meta'
import type { ParsedUrlQuery } from 'querystring'
2022-01-26 07:22:11 +01:00
import type { Rewrite } from '../lib/load-custom-routes'
2021-12-05 22:53:11 +01:00
import type { RenderOpts , RenderOptsPartial } from './render'
import type { ResponseCacheEntry , ResponseCacheValue } from './response-cache'
import type { UrlWithParsedQuery } from 'url'
2021-12-17 23:56:26 +01:00
import type { CacheFs } from '../shared/lib/utils'
2022-01-19 13:36:06 +01:00
import type { PreviewData } from 'next/types'
import type { PagesManifest } from '../build/webpack/plugins/pages-manifest-plugin'
import type { BaseNextRequest , BaseNextResponse } from './base-http'
2022-02-24 20:53:17 +01:00
import type { PayloadOptions } from './send-payload'
2021-12-05 22:53:11 +01:00
2022-01-19 22:54:04 +01:00
import { join , resolve } from 'path'
2022-01-26 07:22:11 +01:00
import { parse as parseQs } from 'querystring'
2021-12-05 22:53:11 +01:00
import { format as formatUrl , parse as parseUrl } from 'url'
2022-01-26 07:22:11 +01:00
import { getRedirectStatus } from '../lib/load-custom-routes'
2021-12-05 22:53:11 +01:00
import {
SERVERLESS_DIRECTORY ,
SERVER_DIRECTORY ,
STATIC_STATUS_PAGES ,
TEMPORARY_REDIRECT_STATUS ,
} from '../shared/lib/constants'
import {
2022-03-28 23:16:43 +02:00
getRoutingItems ,
2021-12-05 22:53:11 +01:00
isDynamicRoute ,
2022-03-28 23:16:43 +02:00
RoutingItem ,
2021-12-05 22:53:11 +01:00
} from '../shared/lib/router/utils'
2022-02-11 20:56:25 +01:00
import {
setLazyProp ,
getCookieParser ,
checkIsManualRevalidate ,
} from './api-utils'
2021-12-05 22:53:11 +01:00
import * as envConfig from '../shared/lib/runtime-config'
2022-01-14 22:01:35 +01:00
import { DecodeError , normalizeRepeatedSlashes } from '../shared/lib/utils'
2022-01-20 22:25:44 +01:00
import { isTargetLikeServerless } from './utils'
2022-03-22 15:54:05 +01:00
import Router , { route } from './router'
2022-02-24 20:53:17 +01:00
import { setRevalidateHeaders } from './send-payload/revalidate-headers'
2021-12-05 22:53:11 +01:00
import { IncrementalCache } from './incremental-cache'
import { execOnce } from '../shared/lib/utils'
import { isBlockedPage , isBot } from './utils'
import RenderResult from './render-result'
import { removePathTrailingSlash } from '../client/normalize-trailing-slash'
import getRouteFromAssetPath from '../shared/lib/router/utils/get-route-from-asset-path'
import { denormalizePagePath } from './denormalize-page-path'
import { normalizeLocalePath } from '../shared/lib/i18n/normalize-locale-path'
import * as Log from '../build/output/log'
import { detectDomainLocale } from '../shared/lib/i18n/detect-domain-locale'
import escapePathDelimiters from '../shared/lib/router/utils/escape-path-delimiters'
import { getUtils } from '../build/webpack/loaders/next-serverless-loader/utils'
import ResponseCache from './response-cache'
import { parseNextUrl } from '../shared/lib/router/utils/parse-next-url'
2022-01-11 21:40:03 +01:00
import isError , { getProperError } from '../lib/is-error'
2021-12-05 22:53:11 +01:00
import { MIDDLEWARE_ROUTE } from '../lib/constants'
import { addRequestMeta , getRequestMeta } from './request-meta'
2022-01-26 07:22:11 +01:00
import { createHeaderRoute , createRedirectRoute } from './server-route-utils'
import { PrerenderManifest } from '../build'
2022-02-22 15:27:18 +01:00
import { ImageConfigComplete } from '../shared/lib/image-config'
2022-03-22 15:54:05 +01:00
import { replaceBasePath } from './router-utils'
2021-12-05 22:53:11 +01:00
export type FindComponentsResult = {
components : LoadComponentsReturnType
query : NextParsedUrlQuery
}
export interface Options {
/ * *
* Object containing the configuration next . config . js
* /
conf : NextConfig
/ * *
* Set to false when the server was created by Next . js
* /
customServer? : boolean
/ * *
* Tells if Next . js is running in dev mode
* /
dev? : boolean
/ * *
* Where the Next project is located
* /
dir? : string
/ * *
* Tells if Next . js is running in a Serverless platform
* /
minimalMode? : boolean
/ * *
* Hide error messages containing server information
* /
quiet? : boolean
/ * *
* The hostname the server is running behind
* /
hostname? : string
/ * *
* The port the server is running behind
* /
port? : number
}
2022-01-14 22:01:35 +01:00
export interface BaseRequestHandler {
2021-12-05 22:53:11 +01:00
(
2022-01-14 22:01:35 +01:00
req : BaseNextRequest ,
res : BaseNextResponse ,
2021-12-05 22:53:11 +01:00
parsedUrl? : NextUrlWithParsedQuery | undefined
) : Promise < void >
}
type RequestContext = {
2022-01-14 22:01:35 +01:00
req : BaseNextRequest
res : BaseNextResponse
2021-12-05 22:53:11 +01:00
pathname : string
query : NextParsedUrlQuery
renderOpts : RenderOptsPartial
}
export default abstract class Server {
protected dir : string
protected quiet : boolean
protected nextConfig : NextConfigComplete
protected distDir : string
protected pagesDir? : string
protected publicDir : string
protected hasStaticDir : boolean
protected pagesManifest? : PagesManifest
protected buildId : string
protected minimalMode : boolean
protected renderOpts : {
poweredByHeader : boolean
buildId : string
generateEtags : boolean
runtimeConfig ? : { [ key : string ] : any }
assetPrefix? : string
canonicalBase : string
dev? : boolean
previewProps : __ApiPreviewProps
customServer? : boolean
ampOptimizerConfig ? : { [ key : string ] : any }
basePath : string
optimizeFonts : boolean
2022-02-09 01:55:53 +01:00
images : ImageConfigComplete
2022-01-12 19:12:36 +01:00
fontManifest? : FontManifest
2021-12-05 22:53:11 +01:00
disableOptimizedLoading? : boolean
optimizeCss : any
2022-03-11 23:26:46 +01:00
nextScriptWorkers : any
2021-12-05 22:53:11 +01:00
locale? : string
locales? : string [ ]
defaultLocale? : string
domainLocales? : DomainLocale [ ]
distDir : string
2022-02-08 14:16:46 +01:00
runtime ? : 'nodejs' | 'edge'
2022-01-14 14:01:00 +01:00
serverComponents? : boolean
2021-12-05 22:53:11 +01:00
crossOrigin? : string
2022-02-01 23:36:47 +01:00
supportsDynamicHTML? : boolean
serverComponentManifest? : any
renderServerComponentData? : boolean
serverComponentProps? : any
2022-02-08 20:39:27 +01:00
reactRoot : boolean
2021-12-05 22:53:11 +01:00
}
private incrementalCache : IncrementalCache
private responseCache : ResponseCache
protected router : Router
protected dynamicRoutes? : DynamicRoutes
protected customRoutes : CustomRoutes
protected middlewareManifest? : MiddlewareManifest
2022-02-08 14:16:46 +01:00
protected serverComponentManifest? : any
2022-03-28 23:16:43 +02:00
protected allRoutes? : RoutingItem [ ]
2021-12-05 22:53:11 +01:00
public readonly hostname? : string
public readonly port? : number
2022-01-21 17:24:57 +01:00
protected abstract getPublicDir ( ) : string
2021-12-07 02:14:55 +01:00
protected abstract getHasStaticDir ( ) : boolean
protected abstract getPagesManifest ( ) : PagesManifest | undefined
protected abstract getBuildId ( ) : string
protected abstract generatePublicRoutes ( ) : Route [ ]
2022-01-12 19:12:36 +01:00
protected abstract generateImageRoutes ( ) : Route [ ]
2022-01-31 23:54:17 +01:00
protected abstract generateStaticRoutes ( ) : Route [ ]
2022-01-19 22:54:04 +01:00
protected abstract generateFsStaticRoutes ( ) : Route [ ]
2022-03-28 23:16:43 +02:00
protected abstract generateCatchAllStaticMiddlewareRoute ( ) : Route | undefined
protected abstract generateCatchAllDynamicMiddlewareRoute ( ) : Route | undefined
2022-01-26 07:22:11 +01:00
protected abstract generateRewrites ( {
restrictedRedirectPaths ,
} : {
restrictedRedirectPaths : string [ ]
} ) : {
beforeFiles : Route [ ]
afterFiles : Route [ ]
fallback : Route [ ]
}
2021-12-07 02:14:55 +01:00
protected abstract getFilesystemPaths ( ) : Set < string >
2022-01-12 19:12:36 +01:00
protected abstract findPageComponents (
pathname : string ,
query? : NextParsedUrlQuery ,
params? : Params | null
) : Promise < FindComponentsResult | null >
2022-01-26 07:22:11 +01:00
protected abstract hasMiddleware (
pathname : string ,
_isSSR? : boolean
) : Promise < boolean >
2022-01-12 19:12:36 +01:00
protected abstract getPagePath ( pathname : string , locales? : string [ ] ) : string
protected abstract getFontManifest ( ) : FontManifest | undefined
2022-01-19 13:36:06 +01:00
protected abstract getMiddlewareManifest ( ) : MiddlewareManifest | undefined
2022-01-21 16:31:47 +01:00
protected abstract getRoutesManifest ( ) : CustomRoutes
2022-01-26 07:22:11 +01:00
protected abstract getPrerenderManifest ( ) : PrerenderManifest
2022-02-08 14:16:46 +01:00
protected abstract getServerComponentManifest ( ) : any
2021-12-07 02:14:55 +01:00
2022-01-14 22:01:35 +01:00
protected abstract sendRenderResult (
req : BaseNextRequest ,
res : BaseNextResponse ,
options : {
result : RenderResult
type : 'html' | 'json'
generateEtags : boolean
poweredByHeader : boolean
options? : PayloadOptions
}
) : Promise < void >
protected abstract runApi (
req : BaseNextRequest ,
res : BaseNextResponse ,
query : ParsedUrlQuery ,
params : Params | boolean ,
page : string ,
builtPagePath : string
) : Promise < boolean >
protected abstract renderHTML (
req : BaseNextRequest ,
res : BaseNextResponse ,
pathname : string ,
query : NextParsedUrlQuery ,
renderOpts : RenderOpts
) : Promise < RenderResult | null >
protected abstract handleCompression (
req : BaseNextRequest ,
res : BaseNextResponse
) : void
2022-01-20 22:25:44 +01:00
protected abstract loadEnvConfig ( params : { dev : boolean } ) : void
2021-12-05 22:53:11 +01:00
public constructor ( {
dir = '.' ,
quiet = false ,
conf ,
dev = false ,
minimalMode = false ,
customServer = true ,
hostname ,
port ,
} : Options ) {
this . dir = resolve ( dir )
this . quiet = quiet
2022-01-20 22:25:44 +01:00
this . loadEnvConfig ( { dev } )
2021-12-05 22:53:11 +01:00
// TODO: should conf be normalized to prevent missing
// values from causing issues as this can be user provided
this . nextConfig = conf as NextConfigComplete
this . hostname = hostname
this . port = port
this . distDir = join ( this . dir , this . nextConfig . distDir )
2022-01-21 17:24:57 +01:00
this . publicDir = this . getPublicDir ( )
2021-12-07 02:14:55 +01:00
this . hasStaticDir = ! minimalMode && this . getHasStaticDir ( )
2021-12-05 22:53:11 +01:00
// Only serverRuntimeConfig needs the default
// publicRuntimeConfig gets it's default in client/index.js
const {
serverRuntimeConfig = { } ,
publicRuntimeConfig ,
assetPrefix ,
generateEtags ,
} = this . nextConfig
2021-12-07 02:14:55 +01:00
this . buildId = this . getBuildId ( )
2022-02-11 18:43:39 +01:00
this . minimalMode = minimalMode || ! ! process . env . NEXT_PRIVATE_MINIMAL_MODE
2021-12-05 22:53:11 +01:00
2022-02-08 14:16:46 +01:00
const serverComponents = this . nextConfig . experimental . serverComponents
this . serverComponentManifest = serverComponents
? this . getServerComponentManifest ( )
: undefined
2021-12-05 22:53:11 +01:00
this . renderOpts = {
poweredByHeader : this.nextConfig.poweredByHeader ,
canonicalBase : this.nextConfig.amp.canonicalBase || '' ,
buildId : this.buildId ,
generateEtags ,
previewProps : this.getPreviewProps ( ) ,
customServer : customServer === true ? true : undefined ,
ampOptimizerConfig : this.nextConfig.experimental.amp?.optimizer ,
basePath : this.nextConfig.basePath ,
2022-02-09 01:55:53 +01:00
images : this.nextConfig.images ,
2021-12-05 22:53:11 +01:00
optimizeFonts : ! ! this . nextConfig . optimizeFonts && ! dev ,
fontManifest :
this . nextConfig . optimizeFonts && ! dev
2022-01-12 19:12:36 +01:00
? this . getFontManifest ( )
: undefined ,
2021-12-05 22:53:11 +01:00
optimizeCss : this.nextConfig.experimental.optimizeCss ,
2022-03-11 23:26:46 +01:00
nextScriptWorkers : this.nextConfig.experimental.nextScriptWorkers ,
2022-02-08 14:16:46 +01:00
disableOptimizedLoading : this.nextConfig.experimental.runtime
? true
: this . nextConfig . experimental . disableOptimizedLoading ,
2021-12-05 22:53:11 +01:00
domainLocales : this.nextConfig.i18n?.domains ,
distDir : this.distDir ,
2022-02-08 14:16:46 +01:00
runtime : this.nextConfig.experimental.runtime ,
serverComponents ,
2021-12-05 22:53:11 +01:00
crossOrigin : this.nextConfig.crossOrigin
? this . nextConfig . crossOrigin
: undefined ,
2022-02-08 20:39:27 +01:00
reactRoot : this.nextConfig.experimental.reactRoot === true ,
2021-12-05 22:53:11 +01:00
}
// Only the `publicRuntimeConfig` key is exposed to the client side
// It'll be rendered as part of __NEXT_DATA__ on the client side
if ( Object . keys ( publicRuntimeConfig ) . length > 0 ) {
this . renderOpts . runtimeConfig = publicRuntimeConfig
}
// Initialize next/config with the environment configuration
envConfig . setConfig ( {
serverRuntimeConfig ,
publicRuntimeConfig ,
} )
2021-12-07 02:14:55 +01:00
this . pagesManifest = this . getPagesManifest ( )
this . middlewareManifest = this . getMiddlewareManifest ( )
2021-12-05 22:53:11 +01:00
this . customRoutes = this . getCustomRoutes ( )
this . router = new Router ( this . generateRoutes ( ) )
this . setAssetPrefix ( assetPrefix )
this . incrementalCache = new IncrementalCache ( {
2021-12-17 23:56:26 +01:00
fs : this.getCacheFilesystem ( ) ,
2021-12-05 22:53:11 +01:00
dev ,
distDir : this.distDir ,
pagesDir : join (
this . distDir ,
this . _isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY ,
'pages'
) ,
locales : this.nextConfig.i18n?.locales ,
max : this.nextConfig.experimental.isrMemoryCacheSize ,
flushToDisk : ! minimalMode && this . nextConfig . experimental . isrFlushToDisk ,
2022-01-26 07:22:11 +01:00
getPrerenderManifest : ( ) = > {
if ( dev ) {
return {
version : - 1 as any , // letting us know this doesn't conform to spec
routes : { } ,
dynamicRoutes : { } ,
notFoundRoutes : [ ] ,
preview : null as any , // `preview` is special case read in next-dev-server
}
} else {
return this . getPrerenderManifest ( )
}
} ,
2021-12-05 22:53:11 +01:00
} )
2022-03-03 00:06:54 +01:00
this . responseCache = new ResponseCache (
this . incrementalCache ,
this . minimalMode
)
2021-12-05 22:53:11 +01:00
}
public logError ( err : Error ) : void {
if ( this . quiet ) return
console . error ( err )
}
private async handleRequest (
2022-01-14 22:01:35 +01:00
req : BaseNextRequest ,
res : BaseNextResponse ,
2021-12-05 22:53:11 +01:00
parsedUrl? : NextUrlWithParsedQuery
) : Promise < void > {
try {
const urlParts = ( req . url || '' ) . split ( '?' )
const urlNoQuery = urlParts [ 0 ]
if ( urlNoQuery ? . match ( /(\\|\/\/)/ ) ) {
const cleanUrl = normalizeRepeatedSlashes ( req . url ! )
2022-01-14 22:01:35 +01:00
res . redirect ( cleanUrl , 308 ) . body ( cleanUrl ) . send ( )
2021-12-05 22:53:11 +01:00
return
}
setLazyProp ( { req : req as any } , 'cookies' , getCookieParser ( req . headers ) )
// Parse url if parsedUrl not provided
if ( ! parsedUrl || typeof parsedUrl !== 'object' ) {
parsedUrl = parseUrl ( req . url ! , true )
}
// Parse the querystring ourselves if the user doesn't handle querystring parsing
if ( typeof parsedUrl . query === 'string' ) {
parsedUrl . query = parseQs ( parsedUrl . query )
}
2021-12-13 19:30:24 +01:00
// When there are hostname and port we build an absolute URL
const initUrl =
this . hostname && this . port
? ` http:// ${ this . hostname } : ${ this . port } ${ req . url } `
: req . url
addRequestMeta ( req , '__NEXT_INIT_URL' , initUrl )
2021-12-05 22:53:11 +01:00
addRequestMeta ( req , '__NEXT_INIT_QUERY' , { . . . parsedUrl . query } )
const url = parseNextUrl ( {
headers : req.headers ,
nextConfig : this.nextConfig ,
url : req.url?.replace ( /^\/+/ , '/' ) ,
} )
if ( url . basePath ) {
req . url = replaceBasePath ( req . url ! , this . nextConfig . basePath )
addRequestMeta ( req , '_nextHadBasePath' , true )
}
if (
this . minimalMode &&
req . headers [ 'x-matched-path' ] &&
typeof req . headers [ 'x-matched-path' ] === 'string'
) {
const reqUrlIsDataUrl = req . url ? . includes ( '/_next/data' )
const matchedPathIsDataUrl =
req . headers [ 'x-matched-path' ] ? . includes ( '/_next/data' )
const isDataUrl = reqUrlIsDataUrl || matchedPathIsDataUrl
let parsedPath = parseUrl (
isDataUrl ? req . url ! : ( req . headers [ 'x-matched-path' ] as string ) ,
true
)
let matchedPathname = parsedPath . pathname !
let matchedPathnameNoExt = isDataUrl
? matchedPathname . replace ( /\.json$/ , '' )
: matchedPathname
if ( this . nextConfig . i18n ) {
const localePathResult = normalizeLocalePath (
matchedPathname || '/' ,
this . nextConfig . i18n . locales
)
if ( localePathResult . detectedLocale ) {
parsedUrl . query . __nextLocale = localePathResult . detectedLocale
}
}
if ( isDataUrl ) {
matchedPathname = denormalizePagePath ( matchedPathname )
matchedPathnameNoExt = denormalizePagePath ( matchedPathnameNoExt )
}
const pageIsDynamic = isDynamicRoute ( matchedPathnameNoExt )
const combinedRewrites : Rewrite [ ] = [ ]
combinedRewrites . push ( . . . this . customRoutes . rewrites . beforeFiles )
combinedRewrites . push ( . . . this . customRoutes . rewrites . afterFiles )
combinedRewrites . push ( . . . this . customRoutes . rewrites . fallback )
const utils = getUtils ( {
pageIsDynamic ,
page : matchedPathnameNoExt ,
i18n : this.nextConfig.i18n ,
basePath : this.nextConfig.basePath ,
rewrites : combinedRewrites ,
} )
try {
// ensure parsedUrl.pathname includes URL before processing
// rewrites or they won't match correctly
if ( this . nextConfig . i18n && ! url . locale ? . path . detectedLocale ) {
parsedUrl . pathname = ` / ${ url . locale ? . locale } ${ parsedUrl . pathname } `
}
utils . handleRewrites ( req , parsedUrl )
// interpolate dynamic params and normalize URL if needed
if ( pageIsDynamic ) {
let params : ParsedUrlQuery | false = { }
Object . assign ( parsedUrl . query , parsedPath . query )
const paramsResult = utils . normalizeDynamicRouteParams (
parsedUrl . query
)
if ( paramsResult . hasValidParams ) {
params = paramsResult . params
} else if ( req . headers [ 'x-now-route-matches' ] ) {
const opts : Record < string , string > = { }
params = utils . getParamsFromRouteMatches (
req ,
opts ,
parsedUrl . query . __nextLocale || ''
)
if ( opts . locale ) {
parsedUrl . query . __nextLocale = opts . locale
}
} else {
params = utils . dynamicRouteMatcher ! ( matchedPathnameNoExt )
}
if ( params ) {
2022-01-22 00:16:24 +01:00
if ( ! paramsResult . hasValidParams ) {
params = utils . normalizeDynamicRouteParams ( params ) . params
}
2021-12-05 22:53:11 +01:00
matchedPathname = utils . interpolateDynamicPath (
matchedPathname ,
params
)
req . url = utils . interpolateDynamicPath ( req . url ! , params )
}
if ( reqUrlIsDataUrl && matchedPathIsDataUrl ) {
req . url = formatUrl ( {
. . . parsedPath ,
pathname : matchedPathname ,
} )
}
Object . assign ( parsedUrl . query , params )
utils . normalizeVercelUrl ( req , true )
}
} catch ( err ) {
if ( err instanceof DecodeError ) {
res . statusCode = 400
return this . renderError ( null , req , res , '/_error' , { } )
}
throw err
}
parsedUrl . pathname = ` ${ this . nextConfig . basePath || '' } ${
matchedPathname === '/' && this . nextConfig . basePath
? ''
: matchedPathname
} `
url . pathname = parsedUrl . pathname
}
addRequestMeta ( req , '__nextHadTrailingSlash' , url . locale ? . trailingSlash )
if ( url . locale ? . domain ) {
addRequestMeta ( req , '__nextIsLocaleDomain' , true )
}
if ( url . locale ? . path . detectedLocale ) {
req . url = formatUrl ( url )
addRequestMeta ( req , '__nextStrippedLocale' , true )
}
if ( ! this . minimalMode || ! parsedUrl . query . __nextLocale ) {
if ( url ? . locale ? . locale ) {
parsedUrl . query . __nextLocale = url . locale . locale
}
}
if ( url ? . locale ? . defaultLocale ) {
parsedUrl . query . __nextDefaultLocale = url . locale . defaultLocale
}
if ( url . locale ? . redirect ) {
2022-01-14 22:01:35 +01:00
res
. redirect ( url . locale . redirect , TEMPORARY_REDIRECT_STATUS )
. body ( url . locale . redirect )
. send ( )
2021-12-05 22:53:11 +01:00
return
}
res . statusCode = 200
return await this . run ( req , res , parsedUrl )
} catch ( err : any ) {
if (
( err && typeof err === 'object' && err . code === 'ERR_INVALID_URL' ) ||
err instanceof DecodeError
) {
res . statusCode = 400
return this . renderError ( null , req , res , '/_error' , { } )
}
if ( this . minimalMode || this . renderOpts . dev ) {
throw err
}
2022-01-11 21:40:03 +01:00
this . logError ( getProperError ( err ) )
2021-12-05 22:53:11 +01:00
res . statusCode = 500
2022-01-14 22:01:35 +01:00
res . body ( 'Internal Server Error' ) . send ( )
2021-12-05 22:53:11 +01:00
}
}
2022-01-14 22:01:35 +01:00
public getRequestHandler ( ) : BaseRequestHandler {
2021-12-05 22:53:11 +01:00
return this . handleRequest . bind ( this )
}
public setAssetPrefix ( prefix? : string ) : void {
this . renderOpts . assetPrefix = prefix ? prefix . replace ( /\/$/ , '' ) : ''
}
// Backwards compatibility
public async prepare ( ) : Promise < void > { }
// Backwards compatibility
protected async close ( ) : Promise < void > { }
protected getCustomRoutes ( ) : CustomRoutes {
2022-01-21 16:31:47 +01:00
const customRoutes = this . getRoutesManifest ( )
2021-12-05 22:53:11 +01:00
let rewrites : CustomRoutes [ 'rewrites' ]
// rewrites can be stored as an array when an array is
// returned in next.config.js so massage them into
// the expected object format
if ( Array . isArray ( customRoutes . rewrites ) ) {
rewrites = {
beforeFiles : [ ] ,
afterFiles : customRoutes.rewrites ,
fallback : [ ] ,
}
} else {
rewrites = customRoutes . rewrites
}
return Object . assign ( customRoutes , { rewrites } )
}
protected getPreviewProps ( ) : __ApiPreviewProps {
return this . getPrerenderManifest ( ) . preview
}
protected async ensureMiddleware ( _pathname : string , _isSSR? : boolean ) { }
protected generateRoutes ( ) : {
basePath : string
headers : Route [ ]
rewrites : {
beforeFiles : Route [ ]
afterFiles : Route [ ]
fallback : Route [ ]
}
fsRoutes : Route [ ]
2022-03-28 23:16:43 +02:00
internalFsRoutes : Route [ ]
2021-12-05 22:53:11 +01:00
redirects : Route [ ]
catchAllRoute : Route
2022-03-28 23:16:43 +02:00
catchAllStaticMiddleware? : Route
catchAllDynamicMiddleware? : Route
2021-12-05 22:53:11 +01:00
pageChecker : PageChecker
useFileSystemPublicRoutes : boolean
dynamicRoutes : DynamicRoutes | undefined
locales : string [ ]
} {
2021-12-07 02:14:55 +01:00
const publicRoutes = this . generatePublicRoutes ( )
2022-01-12 19:12:36 +01:00
const imageRoutes = this . generateImageRoutes ( )
2022-01-31 23:54:17 +01:00
const staticFilesRoutes = this . generateStaticRoutes ( )
2021-12-05 22:53:11 +01:00
2022-03-28 23:16:43 +02:00
const internalFsRoutes : Route [ ] = [
2022-01-19 22:54:04 +01:00
. . . this . generateFsStaticRoutes ( ) ,
2021-12-05 22:53:11 +01:00
{
match : route ( '/_next/data/:path*' ) ,
type : 'route' ,
name : '_next/data catchall' ,
fn : async ( req , res , params , _parsedUrl ) = > {
// Make sure to 404 for /_next/data/ itself and
// we also want to 404 if the buildId isn't correct
if ( ! params . path || params . path [ 0 ] !== this . buildId ) {
await this . render404 ( req , res , _parsedUrl )
return {
finished : true ,
}
}
// remove buildId from URL
params . path . shift ( )
const lastParam = params . path [ params . path . length - 1 ]
// show 404 if it doesn't end with .json
if ( typeof lastParam !== 'string' || ! lastParam . endsWith ( '.json' ) ) {
await this . render404 ( req , res , _parsedUrl )
return {
finished : true ,
}
}
// re-create page's pathname
let pathname = ` / ${ params . path . join ( '/' ) } `
pathname = getRouteFromAssetPath ( pathname , '.json' )
if ( this . nextConfig . i18n ) {
const { host } = req ? . headers || { }
// remove port from host and remove port if present
const hostname = host ? . split ( ':' ) [ 0 ] . toLowerCase ( )
const localePathResult = normalizeLocalePath (
pathname ,
this . nextConfig . i18n . locales
)
const { defaultLocale } =
detectDomainLocale ( this . nextConfig . i18n . domains , hostname ) || { }
let detectedLocale = ''
if ( localePathResult . detectedLocale ) {
pathname = localePathResult . pathname
detectedLocale = localePathResult . detectedLocale
}
_parsedUrl . query . __nextLocale = detectedLocale
_parsedUrl . query . __nextDefaultLocale =
defaultLocale || this . nextConfig . i18n . defaultLocale
if ( ! detectedLocale ) {
_parsedUrl . query . __nextLocale =
_parsedUrl . query . __nextDefaultLocale
await this . render404 ( req , res , _parsedUrl )
return { finished : true }
}
}
const parsedUrl = parseUrl ( pathname , true )
await this . render (
req ,
res ,
pathname ,
{ . . . _parsedUrl . query , _nextDataReq : '1' } ,
2022-01-21 21:38:59 +01:00
parsedUrl ,
true
2021-12-05 22:53:11 +01:00
)
return {
finished : true ,
}
} ,
} ,
2022-01-12 19:12:36 +01:00
. . . imageRoutes ,
2021-12-05 22:53:11 +01:00
{
match : route ( '/_next/:path*' ) ,
type : 'route' ,
name : '_next catchall' ,
// This path is needed because `render()` does a check for `/_next` and the calls the routing again
fn : async ( req , res , _params , parsedUrl ) = > {
await this . render404 ( req , res , parsedUrl )
return {
finished : true ,
}
} ,
} ,
]
2022-03-28 23:16:43 +02:00
const fsRoutes : Route [ ] = [ . . . publicRoutes , . . . staticFilesRoutes ]
2022-01-26 07:22:11 +01:00
const restrictedRedirectPaths = this . nextConfig . basePath
? [ ` ${ this . nextConfig . basePath } /_next ` ]
: [ '/_next' ]
2021-12-05 22:53:11 +01:00
// Headers come very first
const headers = this . minimalMode
? [ ]
2022-01-26 07:22:11 +01:00
: this . customRoutes . headers . map ( ( rule ) = >
createHeaderRoute ( { rule , restrictedRedirectPaths } )
)
2021-12-05 22:53:11 +01:00
const redirects = this . minimalMode
? [ ]
2022-01-26 07:22:11 +01:00
: this . customRoutes . redirects . map ( ( rule ) = >
createRedirectRoute ( { rule , restrictedRedirectPaths } )
2021-12-05 22:53:11 +01:00
)
2022-01-26 07:22:11 +01:00
const rewrites = this . generateRewrites ( { restrictedRedirectPaths } )
2022-03-28 23:16:43 +02:00
const catchAllStaticMiddleware =
this . generateCatchAllStaticMiddlewareRoute ( )
const catchAllDynamicMiddleware =
this . generateCatchAllDynamicMiddlewareRoute ( )
2021-12-05 22:53:11 +01:00
const catchAllRoute : Route = {
match : route ( '/:path*' ) ,
type : 'route' ,
name : 'Catchall render' ,
fn : async ( req , res , _params , parsedUrl ) = > {
let { pathname , query } = parsedUrl
if ( ! pathname ) {
throw new Error ( 'pathname is undefined' )
}
// next.js core assumes page path without trailing slash
pathname = removePathTrailingSlash ( pathname )
if ( this . nextConfig . i18n ) {
const localePathResult = normalizeLocalePath (
pathname ,
this . nextConfig . i18n ? . locales
)
if ( localePathResult . detectedLocale ) {
pathname = localePathResult . pathname
parsedUrl . query . __nextLocale = localePathResult . detectedLocale
}
}
const bubbleNoFallback = ! ! query . _nextBubbleNoFallback
if ( pathname . match ( MIDDLEWARE_ROUTE ) ) {
await this . render404 ( req , res , parsedUrl )
return {
finished : true ,
}
}
2022-03-28 23:16:43 +02:00
if ( isApiRoute ( pathname ) ) {
2021-12-05 22:53:11 +01:00
delete query . _nextBubbleNoFallback
2022-01-14 22:01:35 +01:00
const handled = await this . handleApiRequest ( req , res , pathname , query )
2021-12-05 22:53:11 +01:00
if ( handled ) {
return { finished : true }
}
}
try {
2022-01-21 21:38:59 +01:00
await this . render ( req , res , pathname , query , parsedUrl , true )
2021-12-05 22:53:11 +01:00
return {
finished : true ,
}
} catch ( err ) {
if ( err instanceof NoFallbackError && bubbleNoFallback ) {
return {
finished : false ,
}
}
throw err
}
} ,
}
const { useFileSystemPublicRoutes } = this . nextConfig
if ( useFileSystemPublicRoutes ) {
2022-03-28 23:16:43 +02:00
this . allRoutes = this . getAllRoutes ( )
2021-12-05 22:53:11 +01:00
this . dynamicRoutes = this . getDynamicRoutes ( )
}
return {
headers ,
fsRoutes ,
2022-03-28 23:16:43 +02:00
internalFsRoutes ,
2022-01-26 07:22:11 +01:00
rewrites ,
2021-12-05 22:53:11 +01:00
redirects ,
catchAllRoute ,
2022-03-28 23:16:43 +02:00
catchAllStaticMiddleware ,
catchAllDynamicMiddleware ,
2021-12-05 22:53:11 +01:00
useFileSystemPublicRoutes ,
dynamicRoutes : this.dynamicRoutes ,
basePath : this.nextConfig.basePath ,
pageChecker : this.hasPage.bind ( this ) ,
locales : this.nextConfig.i18n?.locales || [ ] ,
}
}
protected async hasPage ( pathname : string ) : Promise < boolean > {
let found = false
try {
2022-01-12 19:12:36 +01:00
found = ! ! this . getPagePath ( pathname , this . nextConfig . i18n ? . locales )
2021-12-05 22:53:11 +01:00
} catch ( _ ) { }
return found
}
protected async _beforeCatchAllRender (
2022-01-14 22:01:35 +01:00
_req : BaseNextRequest ,
_res : BaseNextResponse ,
2021-12-05 22:53:11 +01:00
_params : Params ,
_parsedUrl : UrlWithParsedQuery
) : Promise < boolean > {
return false
}
// Used to build API page in development
protected async ensureApiPage ( _pathname : string ) : Promise < void > { }
/ * *
* Resolves ` API ` request , in development builds on demand
* @param req http request
* @param res http response
* @param pathname path of request
* /
private async handleApiRequest (
2022-01-14 22:01:35 +01:00
req : BaseNextRequest ,
res : BaseNextResponse ,
2021-12-05 22:53:11 +01:00
pathname : string ,
query : ParsedUrlQuery
) : Promise < boolean > {
let page = pathname
2022-01-14 22:01:35 +01:00
let params : Params | false = false
2022-02-02 03:57:04 +01:00
let pageFound = ! isDynamicRoute ( page ) && ( await this . hasPage ( page ) )
2021-12-05 22:53:11 +01:00
if ( ! pageFound && this . dynamicRoutes ) {
for ( const dynamicRoute of this . dynamicRoutes ) {
params = dynamicRoute . match ( pathname )
if ( dynamicRoute . page . startsWith ( '/api' ) && params ) {
page = dynamicRoute . page
pageFound = true
break
}
}
}
if ( ! pageFound ) {
return false
}
// Make sure the page is built before getting the path
// or else it won't be in the manifest yet
await this . ensureApiPage ( page )
let builtPagePath
try {
2022-01-12 19:12:36 +01:00
builtPagePath = this . getPagePath ( page )
2021-12-05 22:53:11 +01:00
} catch ( err ) {
if ( isError ( err ) && err . code === 'ENOENT' ) {
return false
}
throw err
}
2022-01-14 22:01:35 +01:00
return this . runApi ( req , res , query , params , page , builtPagePath )
2021-12-05 22:53:11 +01:00
}
2022-03-28 23:16:43 +02:00
protected getAllRoutes ( ) : RoutingItem [ ] {
const pages = Object . keys ( this . pagesManifest ! ) . map (
( page ) = >
normalizeLocalePath ( page , this . nextConfig . i18n ? . locales ) . pathname
)
const middlewareMap = this . minimalMode
? { }
: this . middlewareManifest ? . middleware || { }
const middleware = Object . keys ( middlewareMap ) . map ( ( page ) = > ( {
page ,
ssr : ! MIDDLEWARE_ROUTE . test ( middlewareMap [ page ] . name ) ,
} ) )
return getRoutingItems ( pages , middleware )
}
2021-12-05 22:53:11 +01:00
protected getDynamicRoutes ( ) : Array < RoutingItem > {
const addedPages = new Set < string > ( )
2022-03-28 23:16:43 +02:00
return this . allRoutes ! . filter ( ( item ) = > {
if (
item . isMiddleware ||
addedPages . has ( item . page ) ||
! isDynamicRoute ( item . page )
2021-12-05 22:53:11 +01:00
)
2022-03-28 23:16:43 +02:00
return false
addedPages . add ( item . page )
return true
} )
2021-12-05 22:53:11 +01:00
}
protected async run (
2022-01-14 22:01:35 +01:00
req : BaseNextRequest ,
res : BaseNextResponse ,
2021-12-05 22:53:11 +01:00
parsedUrl : UrlWithParsedQuery
) : Promise < void > {
this . handleCompression ( req , res )
try {
const matched = await this . router . execute ( req , res , parsedUrl )
if ( matched ) {
return
}
} catch ( err ) {
if ( err instanceof DecodeError ) {
res . statusCode = 400
return this . renderError ( null , req , res , '/_error' , { } )
}
throw err
}
await this . render404 ( req , res , parsedUrl )
}
private async pipe (
fn : ( ctx : RequestContext ) = > Promise < ResponsePayload | null > ,
partialContext : {
2022-01-14 22:01:35 +01:00
req : BaseNextRequest
res : BaseNextResponse
2021-12-05 22:53:11 +01:00
pathname : string
query : NextParsedUrlQuery
}
) : Promise < void > {
2022-03-04 08:39:26 +01:00
const isBotRequest = isBot ( partialContext . req . headers [ 'user-agent' ] || '' )
2021-12-05 22:53:11 +01:00
const ctx = {
. . . partialContext ,
renderOpts : {
. . . this . renderOpts ,
2022-03-04 08:39:26 +01:00
supportsDynamicHTML : ! isBotRequest ,
2021-12-05 22:53:11 +01:00
} ,
} as const
const payload = await fn ( ctx )
if ( payload === null ) {
return
}
const { req , res } = ctx
const { body , type , revalidateOptions } = payload
2022-01-14 22:01:35 +01:00
if ( ! res . sent ) {
2021-12-05 22:53:11 +01:00
const { generateEtags , poweredByHeader , dev } = this . renderOpts
if ( dev ) {
// In dev, we should not cache pages for any reason.
res . setHeader ( 'Cache-Control' , 'no-store, must-revalidate' )
}
2022-01-14 22:01:35 +01:00
return this . sendRenderResult ( req , res , {
2021-12-05 22:53:11 +01:00
result : body ,
type ,
generateEtags ,
poweredByHeader ,
options : revalidateOptions ,
} )
}
}
private async getStaticHTML (
fn : ( ctx : RequestContext ) = > Promise < ResponsePayload | null > ,
partialContext : {
2022-01-14 22:01:35 +01:00
req : BaseNextRequest
res : BaseNextResponse
2021-12-05 22:53:11 +01:00
pathname : string
query : ParsedUrlQuery
}
) : Promise < string | null > {
const payload = await fn ( {
. . . partialContext ,
renderOpts : {
. . . this . renderOpts ,
supportsDynamicHTML : false ,
} ,
} )
if ( payload === null ) {
return null
}
return payload . body . toUnchunkedString ( )
}
public async render (
2022-01-14 22:01:35 +01:00
req : BaseNextRequest ,
res : BaseNextResponse ,
2021-12-05 22:53:11 +01:00
pathname : string ,
query : NextParsedUrlQuery = { } ,
2022-01-21 21:38:59 +01:00
parsedUrl? : NextUrlWithParsedQuery ,
internalRender = false
2021-12-05 22:53:11 +01:00
) : Promise < void > {
if ( ! pathname . startsWith ( '/' ) ) {
console . warn (
` Cannot render page with path " ${ pathname } ", did you mean "/ ${ pathname } "?. See more info here: https://nextjs.org/docs/messages/render-no-starting-slash `
)
}
if (
this . renderOpts . customServer &&
pathname === '/index' &&
! ( await this . hasPage ( '/index' ) )
) {
// maintain backwards compatibility for custom server
// (see custom-server integration tests)
pathname = '/'
}
// we allow custom servers to call render for all URLs
// so check if we need to serve a static _next file or not.
// we don't modify the URL for _next/data request but still
// call render so we special case this to prevent an infinite loop
if (
2022-01-21 21:38:59 +01:00
! internalRender &&
2021-12-05 22:53:11 +01:00
! this . minimalMode &&
! query . _nextDataReq &&
( req . url ? . match ( /^\/_next\// ) ||
( this . hasStaticDir && req . url ! . match ( /^\/static\// ) ) )
) {
return this . handleRequest ( req , res , parsedUrl )
}
// Custom server users can run `app.render()` which needs compression.
if ( this . renderOpts . customServer ) {
this . handleCompression ( req , res )
}
if ( isBlockedPage ( pathname ) ) {
return this . render404 ( req , res , parsedUrl )
}
return this . pipe ( ( ctx ) = > this . renderToResponse ( ctx ) , {
req ,
res ,
pathname ,
query ,
} )
}
protected async getStaticPaths ( pathname : string ) : Promise < {
staticPaths : string [ ] | undefined
fallbackMode : 'static' | 'blocking' | false
} > {
// `staticPaths` is intentionally set to `undefined` as it should've
// been caught when checking disk data.
const staticPaths = undefined
// Read whether or not fallback should exist from the manifest.
const fallbackField =
this . getPrerenderManifest ( ) . dynamicRoutes [ pathname ] . fallback
return {
staticPaths ,
fallbackMode :
typeof fallbackField === 'string'
? 'static'
: fallbackField === null
? 'blocking'
: false ,
}
}
private async renderToResponseWithComponents (
{ req , res , pathname , renderOpts : opts } : RequestContext ,
{ components , query } : FindComponentsResult
) : Promise < ResponsePayload | null > {
const is404Page = pathname === '/404'
const is500Page = pathname === '/500'
const isLikeServerless =
typeof components . ComponentMod === 'object' &&
typeof ( components . ComponentMod as any ) . renderReqToHTML === 'function'
const isSSG = ! ! components . getStaticProps
const hasServerProps = ! ! components . getServerSideProps
const hasStaticPaths = ! ! components . getStaticPaths
2022-02-18 16:25:10 +01:00
const hasGetInitialProps = ! ! components . Component ? . getInitialProps
2021-12-05 22:53:11 +01:00
// Toggle whether or not this is a Data request
const isDataReq = ! ! query . _nextDataReq && ( isSSG || hasServerProps )
delete query . _nextDataReq
2022-02-10 22:13:52 +01:00
// Don't delete query.__flight__ yet, it still needs to be used in renderToHTML later
const isFlightRequest = Boolean (
this . serverComponentManifest && query . __flight__
)
2021-12-05 22:53:11 +01:00
// we need to ensure the status code if /404 is visited directly
2022-02-10 22:13:52 +01:00
if ( is404Page && ! isDataReq && ! isFlightRequest ) {
2021-12-05 22:53:11 +01:00
res . statusCode = 404
}
// ensure correct status is set when visiting a status page
// directly e.g. /500
if ( STATIC_STATUS_PAGES . includes ( pathname ) ) {
2022-03-24 22:49:38 +01:00
res . statusCode = parseInt ( pathname . slice ( 1 ) , 10 )
2021-12-05 22:53:11 +01:00
}
2022-02-15 18:28:18 +01:00
// static pages can only respond to GET/HEAD
// requests so ensure we respond with 405 for
// invalid requests
if (
! is404Page &&
! is500Page &&
pathname !== '/_error' &&
req . method !== 'HEAD' &&
req . method !== 'GET' &&
( typeof components . Component === 'string' || isSSG )
) {
res . statusCode = 405
res . setHeader ( 'Allow' , [ 'GET' , 'HEAD' ] )
await this . renderError ( null , req , res , pathname )
return null
}
2021-12-05 22:53:11 +01:00
// handle static page
if ( typeof components . Component === 'string' ) {
return {
type : 'html' ,
// TODO: Static pages should be serialized as RenderResult
body : RenderResult.fromStatic ( components . Component ) ,
}
}
if ( ! query . amp ) {
delete query . amp
}
if ( opts . supportsDynamicHTML === true ) {
2022-03-04 08:39:26 +01:00
const isBotRequest = isBot ( req . headers [ 'user-agent' ] || '' )
2022-03-11 21:38:09 +01:00
const isSupportedDocument =
typeof components . Document ? . getInitialProps !== 'function' ||
// When concurrent features is enabled, the built-in `Document`
// component also supports dynamic HTML.
( this . renderOpts . reactRoot &&
! ! ( components . Document as any ) ? . __next_internal_document )
2021-12-05 22:53:11 +01:00
// Disable dynamic HTML in cases that we know it won't be generated,
// so that we can continue generating a cache key when possible.
opts . supportsDynamicHTML =
! isSSG &&
! isLikeServerless &&
2022-03-04 08:39:26 +01:00
! isBotRequest &&
2021-12-05 22:53:11 +01:00
! query . amp &&
2022-03-11 21:38:09 +01:00
isSupportedDocument
2021-12-05 22:53:11 +01:00
}
const defaultLocale = isSSG
? this . nextConfig . i18n ? . defaultLocale
: query . __nextDefaultLocale
const locale = query . __nextLocale
const locales = this . nextConfig . i18n ? . locales
let previewData : PreviewData
let isPreviewMode = false
if ( hasServerProps || isSSG ) {
2022-02-11 20:56:25 +01:00
// For the edge runtime, we don't support preview mode in SSG.
if ( ! process . browser ) {
const { tryGetPreviewData } =
require ( './api-utils/node' ) as typeof import ( './api-utils/node' )
previewData = tryGetPreviewData ( req , res , this . renderOpts . previewProps )
isPreviewMode = previewData !== false
}
2021-12-05 22:53:11 +01:00
}
2022-02-08 04:50:23 +01:00
let isManualRevalidate = false
if ( isSSG ) {
isManualRevalidate = checkIsManualRevalidate (
req ,
this . renderOpts . previewProps
)
}
2021-12-05 22:53:11 +01:00
// Compute the iSSG cache key. We use the rewroteUrl since
// pages with fallback: false are allowed to be rewritten to
// and we need to look up the path by the rewritten path
let urlPathname = parseUrl ( req . url || '' ) . pathname || '/'
let resolvedUrlPathname =
getRequestMeta ( req , '_nextRewroteUrl' ) || urlPathname
urlPathname = removePathTrailingSlash ( urlPathname )
resolvedUrlPathname = normalizeLocalePath (
removePathTrailingSlash ( resolvedUrlPathname ) ,
this . nextConfig . i18n ? . locales
) . pathname
const stripNextDataPath = ( path : string ) = > {
if ( path . includes ( this . buildId ) ) {
const splitPath = path . substring (
path . indexOf ( this . buildId ) + this . buildId . length
)
path = denormalizePagePath ( splitPath . replace ( /\.json$/ , '' ) )
}
if ( this . nextConfig . i18n ) {
return normalizeLocalePath ( path , locales ) . pathname
}
return path
}
const handleRedirect = ( pageData : any ) = > {
const redirect = {
destination : pageData.pageProps.__N_REDIRECT ,
statusCode : pageData.pageProps.__N_REDIRECT_STATUS ,
basePath : pageData.pageProps.__N_REDIRECT_BASE_PATH ,
}
const statusCode = getRedirectStatus ( redirect )
const { basePath } = this . nextConfig
if (
basePath &&
redirect . basePath !== false &&
redirect . destination . startsWith ( '/' )
) {
redirect . destination = ` ${ basePath } ${ redirect . destination } `
}
if ( redirect . destination . startsWith ( '/' ) ) {
redirect . destination = normalizeRepeatedSlashes ( redirect . destination )
}
2022-01-14 22:01:35 +01:00
res
. redirect ( redirect . destination , statusCode )
. body ( redirect . destination )
. send ( )
2021-12-05 22:53:11 +01:00
}
// remove /_next/data prefix from urlPathname so it matches
// for direct page visit and /_next/data visit
if ( isDataReq ) {
resolvedUrlPathname = stripNextDataPath ( resolvedUrlPathname )
urlPathname = stripNextDataPath ( urlPathname )
}
let ssgCacheKey =
2022-03-03 00:06:54 +01:00
isPreviewMode || ! isSSG || opts . supportsDynamicHTML
2022-02-08 04:50:23 +01:00
? null // Preview mode and manual revalidate bypasses the cache
2021-12-05 22:53:11 +01:00
: ` ${ locale ? ` / ${ locale } ` : '' } ${
( pathname === '/' || resolvedUrlPathname === '/' ) && locale
? ''
: resolvedUrlPathname
} $ { query . amp ? '.amp' : '' } `
if ( ( is404Page || is500Page ) && isSSG ) {
ssgCacheKey = ` ${ locale ? ` / ${ locale } ` : '' } ${ pathname } ${
query . amp ? '.amp' : ''
} `
}
if ( ssgCacheKey ) {
// we only encode path delimiters for path segments from
// getStaticPaths so we need to attempt decoding the URL
// to match against and only escape the path delimiters
// this allows non-ascii values to be handled e.g. Japanese characters
// TODO: investigate adding this handling for non-SSG pages so
// non-ascii names work there also
ssgCacheKey = ssgCacheKey
. split ( '/' )
. map ( ( seg ) = > {
try {
seg = escapePathDelimiters ( decodeURIComponent ( seg ) , true )
} catch ( _ ) {
// An improperly encoded URL was provided
throw new DecodeError ( 'failed to decode param' )
}
return seg
} )
. join ( '/' )
2022-03-23 15:28:04 +01:00
// ensure /index and / is normalized to one key
ssgCacheKey =
ssgCacheKey === '/index' && pathname === '/' ? '/' : ssgCacheKey
2021-12-05 22:53:11 +01:00
}
const doRender : ( ) = > Promise < ResponseCacheEntry | null > = async ( ) = > {
let pageData : any
let body : RenderResult | null
let sprRevalidate : number | false
let isNotFound : boolean | undefined
let isRedirect : boolean | undefined
// handle serverless
if ( isLikeServerless ) {
const renderResult = await (
components . ComponentMod as any
) . renderReqToHTML ( req , res , 'passthrough' , {
locale ,
locales ,
defaultLocale ,
optimizeCss : this.renderOpts.optimizeCss ,
2022-03-11 23:26:46 +01:00
nextScriptWorkers : this.renderOpts.nextScriptWorkers ,
2021-12-05 22:53:11 +01:00
distDir : this.distDir ,
fontManifest : this.renderOpts.fontManifest ,
domainLocales : this.renderOpts.domainLocales ,
} )
body = renderResult . html
pageData = renderResult . renderOpts . pageData
sprRevalidate = renderResult . renderOpts . revalidate
isNotFound = renderResult . renderOpts . isNotFound
isRedirect = renderResult . renderOpts . isRedirect
} else {
const origQuery = parseUrl ( req . url || '' , true ) . query
const hadTrailingSlash =
urlPathname !== '/' && this . nextConfig . trailingSlash
const resolvedUrl = formatUrl ( {
pathname : ` ${ resolvedUrlPathname } ${ hadTrailingSlash ? '/' : '' } ` ,
// make sure to only add query values from original URL
query : origQuery ,
} )
const renderOpts : RenderOpts = {
. . . components ,
. . . opts ,
isDataReq ,
resolvedUrl ,
locale ,
locales ,
defaultLocale ,
// For getServerSideProps and getInitialProps we need to ensure we use the original URL
// and not the resolved URL to prevent a hydration mismatch on
// asPath
resolvedAsPath :
hasServerProps || hasGetInitialProps
? formatUrl ( {
// we use the original URL pathname less the _next/data prefix if
// present
pathname : ` ${ urlPathname } ${ hadTrailingSlash ? '/' : '' } ` ,
query : origQuery ,
} )
: resolvedUrl ,
}
2022-01-14 22:01:35 +01:00
const renderResult = await this . renderHTML (
2021-12-05 22:53:11 +01:00
req ,
res ,
pathname ,
query ,
renderOpts
)
body = renderResult
// TODO: change this to a different passing mechanism
pageData = ( renderOpts as any ) . pageData
sprRevalidate = ( renderOpts as any ) . revalidate
isNotFound = ( renderOpts as any ) . isNotFound
isRedirect = ( renderOpts as any ) . isRedirect
}
let value : ResponseCacheValue | null
if ( isNotFound ) {
value = null
} else if ( isRedirect ) {
value = { kind : 'REDIRECT' , props : pageData }
} else {
if ( ! body ) {
return null
}
value = { kind : 'PAGE' , html : body , pageData }
}
return { revalidate : sprRevalidate , value }
}
const cacheEntry = await this . responseCache . get (
ssgCacheKey ,
2022-02-08 04:50:23 +01:00
async ( hasResolved , hadCache ) = > {
2021-12-05 22:53:11 +01:00
const isProduction = ! this . renderOpts . dev
const isDynamicPathname = isDynamicRoute ( pathname )
2022-01-14 22:01:35 +01:00
const didRespond = hasResolved || res . sent
2021-12-05 22:53:11 +01:00
let { staticPaths , fallbackMode } = hasStaticPaths
? await this . getStaticPaths ( pathname )
: { staticPaths : undefined , fallbackMode : false }
if (
fallbackMode === 'static' &&
isBot ( req . headers [ 'user-agent' ] || '' )
) {
fallbackMode = 'blocking'
}
2022-02-08 04:50:23 +01:00
// only allow manual revalidate for fallback: true/blocking
// or for prerendered fallback: false paths
if ( isManualRevalidate && ( fallbackMode !== false || hadCache ) ) {
fallbackMode = 'blocking'
}
2021-12-05 22:53:11 +01:00
// When we did not respond from cache, we need to choose to block on
// rendering or return a skeleton.
//
// * Data requests always block.
//
// * Blocking mode fallback always blocks.
//
// * Preview mode toggles all pages to be resolved in a blocking manner.
//
// * Non-dynamic pages should block (though this is an impossible
// case in production).
//
// * Dynamic pages should return their skeleton if not defined in
// getStaticPaths, then finish the data request on the client-side.
//
if (
this . minimalMode !== true &&
fallbackMode !== 'blocking' &&
ssgCacheKey &&
! didRespond &&
! isPreviewMode &&
isDynamicPathname &&
// Development should trigger fallback when the path is not in
// `getStaticPaths`
( isProduction ||
! staticPaths ||
! staticPaths . includes (
// we use ssgCacheKey here as it is normalized to match the
// encoding from getStaticPaths along with including the locale
query . amp ? ssgCacheKey . replace ( /\.amp$/ , '' ) : ssgCacheKey
) )
) {
if (
// In development, fall through to render to handle missing
// getStaticPaths.
( isProduction || staticPaths ) &&
// When fallback isn't present, abort this render so we 404
fallbackMode !== 'static'
) {
throw new NoFallbackError ( )
}
if ( ! isDataReq ) {
// Production already emitted the fallback as static HTML.
if ( isProduction ) {
const html = await this . incrementalCache . getFallback (
locale ? ` / ${ locale } ${ pathname } ` : pathname
)
return {
value : {
kind : 'PAGE' ,
html : RenderResult.fromStatic ( html ) ,
pageData : { } ,
} ,
}
}
// We need to generate the fallback on-demand for development.
else {
query . __nextFallback = 'true'
if ( isLikeServerless ) {
prepareServerlessUrl ( req , query )
}
const result = await doRender ( )
if ( ! result ) {
return null
}
// Prevent caching this result
delete result . revalidate
return result
}
}
}
const result = await doRender ( )
if ( ! result ) {
return null
}
return {
. . . result ,
revalidate :
result . revalidate !== undefined
? result . revalidate
: /* default to minimum revalidate (this should be an invariant) */ 1 ,
}
2022-02-08 04:50:23 +01:00
} ,
{
isManualRevalidate ,
2021-12-05 22:53:11 +01:00
}
)
if ( ! cacheEntry ) {
if ( ssgCacheKey ) {
// A cache entry might not be generated if a response is written
// in `getInitialProps` or `getServerSideProps`, but those shouldn't
// have a cache key. If we do have a cache key but we don't end up
// with a cache entry, then either Next.js or the application has a
// bug that needs fixing.
throw new Error ( 'invariant: cache entry required but not generated' )
}
return null
}
2022-02-25 23:17:07 +01:00
if ( isSSG ) {
// set x-nextjs-cache header to match the header
// we set for the image-optimizer
res . setHeader (
'x-nextjs-cache' ,
isManualRevalidate
? 'REVALIDATED'
: cacheEntry . isMiss
? 'MISS'
: cacheEntry . isStale
? 'STALE'
: 'HIT'
)
}
2021-12-05 22:53:11 +01:00
const { revalidate , value : cachedData } = cacheEntry
const revalidateOptions : any =
typeof revalidate !== 'undefined' &&
( ! this . renderOpts . dev || ( hasServerProps && ! isDataReq ) )
? {
// When the page is 404 cache-control should not be added unless
// we are rendering the 404 page for notFound: true which should
// cache according to revalidate correctly
private : isPreviewMode || ( is404Page && cachedData ) ,
stateful : ! isSSG ,
revalidate ,
}
: undefined
if ( ! cachedData ) {
if ( revalidateOptions ) {
setRevalidateHeaders ( res , revalidateOptions )
}
if ( isDataReq ) {
res . statusCode = 404
2022-01-14 22:01:35 +01:00
res . body ( '{"notFound":true}' ) . send ( )
2021-12-05 22:53:11 +01:00
return null
} else {
2022-02-16 19:53:48 +01:00
if ( this . renderOpts . dev ) {
query . __nextNotFoundSrcPage = pathname
}
2021-12-05 22:53:11 +01:00
await this . render404 (
req ,
res ,
{
pathname ,
query ,
} as UrlWithParsedQuery ,
false
)
return null
}
} else if ( cachedData . kind === 'REDIRECT' ) {
if ( isDataReq ) {
return {
type : 'json' ,
body : RenderResult.fromStatic ( JSON . stringify ( cachedData . props ) ) ,
revalidateOptions ,
}
} else {
await handleRedirect ( cachedData . props )
return null
}
2022-02-09 00:46:59 +01:00
} else if ( cachedData . kind === 'IMAGE' ) {
throw new Error ( 'invariant SSG should not return an image cache value' )
2021-12-05 22:53:11 +01:00
} else {
return {
type : isDataReq ? 'json' : 'html' ,
body : isDataReq
? RenderResult . fromStatic ( JSON . stringify ( cachedData . pageData ) )
: cachedData . html ,
revalidateOptions ,
}
}
}
private async renderToResponse (
ctx : RequestContext
) : Promise < ResponsePayload | null > {
const { res , query , pathname } = ctx
let page = pathname
const bubbleNoFallback = ! ! query . _nextBubbleNoFallback
delete query . _nextBubbleNoFallback
try {
2022-02-02 03:57:04 +01:00
// Ensure a request to the URL /accounts/[id] will be treated as a dynamic
// route correctly and not loaded immediately without parsing params.
if ( ! isDynamicRoute ( pathname ) ) {
const result = await this . findPageComponents ( pathname , query )
if ( result ) {
try {
return await this . renderToResponseWithComponents ( ctx , result )
} catch ( err ) {
const isNoFallbackError = err instanceof NoFallbackError
2021-12-05 22:53:11 +01:00
2022-02-02 03:57:04 +01:00
if ( ! isNoFallbackError || ( isNoFallbackError && bubbleNoFallback ) ) {
throw err
}
2021-12-05 22:53:11 +01:00
}
}
}
if ( this . dynamicRoutes ) {
for ( const dynamicRoute of this . dynamicRoutes ) {
const params = dynamicRoute . match ( pathname )
if ( ! params ) {
continue
}
const dynamicRouteResult = await this . findPageComponents (
dynamicRoute . page ,
query ,
params
)
if ( dynamicRouteResult ) {
try {
page = dynamicRoute . page
return await this . renderToResponseWithComponents (
{
. . . ctx ,
pathname : dynamicRoute.page ,
renderOpts : {
. . . ctx . renderOpts ,
params ,
} ,
} ,
dynamicRouteResult
)
} catch ( err ) {
const isNoFallbackError = err instanceof NoFallbackError
if (
! isNoFallbackError ||
( isNoFallbackError && bubbleNoFallback )
) {
throw err
}
}
}
}
}
} catch ( error ) {
2022-01-11 21:40:03 +01:00
const err = getProperError ( error )
2021-12-05 22:53:11 +01:00
if ( err instanceof NoFallbackError && bubbleNoFallback ) {
throw err
}
if ( err instanceof DecodeError ) {
res . statusCode = 400
return await this . renderErrorToResponse ( ctx , err )
}
res . statusCode = 500
const isWrappedError = err instanceof WrappedBuildError
const response = await this . renderErrorToResponse (
ctx ,
isWrappedError ? ( err as WrappedBuildError ) . innerError : err
)
if ( ! isWrappedError ) {
2022-01-26 07:22:11 +01:00
if ( ( this . minimalMode && ! process . browser ) || this . renderOpts . dev ) {
2021-12-05 22:53:11 +01:00
if ( isError ( err ) ) err . page = page
throw err
}
2022-01-11 21:40:03 +01:00
this . logError ( getProperError ( err ) )
2021-12-05 22:53:11 +01:00
}
return response
}
res . statusCode = 404
return this . renderErrorToResponse ( ctx , null )
}
public async renderToHTML (
2022-01-14 22:01:35 +01:00
req : BaseNextRequest ,
res : BaseNextResponse ,
2021-12-05 22:53:11 +01:00
pathname : string ,
query : ParsedUrlQuery = { }
) : Promise < string | null > {
return this . getStaticHTML ( ( ctx ) = > this . renderToResponse ( ctx ) , {
req ,
res ,
pathname ,
query ,
} )
}
public async renderError (
err : Error | null ,
2022-01-14 22:01:35 +01:00
req : BaseNextRequest ,
res : BaseNextResponse ,
2021-12-05 22:53:11 +01:00
pathname : string ,
query : NextParsedUrlQuery = { } ,
setHeaders = true
) : Promise < void > {
if ( setHeaders ) {
res . setHeader (
'Cache-Control' ,
'no-cache, no-store, max-age=0, must-revalidate'
)
}
return this . pipe (
async ( ctx ) = > {
const response = await this . renderErrorToResponse ( ctx , err )
if ( this . minimalMode && res . statusCode === 500 ) {
throw err
}
return response
} ,
{ req , res , pathname , query }
)
}
private customErrorNo404Warn = execOnce ( ( ) = > {
Log . warn (
` You have added a custom /_error page without a custom /404 page. This prevents the 404 page from being auto statically optimized. \ nSee here for info: https://nextjs.org/docs/messages/custom-error-no-custom-404 `
)
} )
private async renderErrorToResponse (
ctx : RequestContext ,
2022-01-11 21:40:03 +01:00
err : Error | null
2021-12-05 22:53:11 +01:00
) : Promise < ResponsePayload | null > {
const { res , query } = ctx
try {
let result : null | FindComponentsResult = null
const is404 = res . statusCode === 404
let using404Page = false
// use static 404 page if available and is 404 response
if ( is404 ) {
result = await this . findPageComponents ( '/404' , query )
using404Page = result !== null
}
let statusPage = ` / ${ res . statusCode } `
if ( ! result && STATIC_STATUS_PAGES . includes ( statusPage ) ) {
result = await this . findPageComponents ( statusPage , query )
}
if ( ! result ) {
result = await this . findPageComponents ( '/_error' , query )
statusPage = '/_error'
}
if (
process . env . NODE_ENV !== 'production' &&
! using404Page &&
( await this . hasPage ( '/_error' ) ) &&
! ( await this . hasPage ( '/404' ) )
) {
this . customErrorNo404Warn ( )
}
try {
return await this . renderToResponseWithComponents (
{
. . . ctx ,
pathname : statusPage ,
renderOpts : {
. . . ctx . renderOpts ,
err ,
} ,
} ,
result !
)
} catch ( maybeFallbackError ) {
if ( maybeFallbackError instanceof NoFallbackError ) {
throw new Error ( 'invariant: failed to render error page' )
}
throw maybeFallbackError
}
} catch ( error ) {
2022-01-11 21:40:03 +01:00
const renderToHtmlError = getProperError ( error )
2021-12-05 22:53:11 +01:00
const isWrappedError = renderToHtmlError instanceof WrappedBuildError
if ( ! isWrappedError ) {
2022-01-11 21:40:03 +01:00
this . logError ( renderToHtmlError )
2021-12-05 22:53:11 +01:00
}
res . statusCode = 500
const fallbackComponents = await this . getFallbackErrorComponents ( )
if ( fallbackComponents ) {
return this . renderToResponseWithComponents (
{
. . . ctx ,
pathname : '/_error' ,
renderOpts : {
. . . ctx . renderOpts ,
// We render `renderToHtmlError` here because `err` is
// already captured in the stacktrace.
err : isWrappedError
? renderToHtmlError . innerError
: renderToHtmlError ,
} ,
} ,
{
query ,
components : fallbackComponents ,
}
)
}
return {
type : 'html' ,
body : RenderResult.fromStatic ( 'Internal Server Error' ) ,
}
}
}
public async renderErrorToHTML (
err : Error | null ,
2022-01-14 22:01:35 +01:00
req : BaseNextRequest ,
res : BaseNextResponse ,
2021-12-05 22:53:11 +01:00
pathname : string ,
query : ParsedUrlQuery = { }
) : Promise < string | null > {
return this . getStaticHTML ( ( ctx ) = > this . renderErrorToResponse ( ctx , err ) , {
req ,
res ,
pathname ,
query ,
} )
}
2021-12-17 23:56:26 +01:00
protected getCacheFilesystem ( ) : CacheFs {
return {
readFile : ( ) = > Promise . resolve ( '' ) ,
readFileSync : ( ) = > '' ,
writeFile : ( ) = > Promise . resolve ( ) ,
mkdir : ( ) = > Promise . resolve ( ) ,
stat : ( ) = > Promise . resolve ( { mtime : new Date ( ) } ) ,
}
}
2021-12-05 22:53:11 +01:00
protected async getFallbackErrorComponents ( ) : Promise < LoadComponentsReturnType | null > {
// The development server will provide an implementation for this
return null
}
public async render404 (
2022-01-14 22:01:35 +01:00
req : BaseNextRequest ,
res : BaseNextResponse ,
2021-12-05 22:53:11 +01:00
parsedUrl? : NextUrlWithParsedQuery ,
setHeaders = true
) : Promise < void > {
const { pathname , query } : NextUrlWithParsedQuery = parsedUrl
? parsedUrl
: parseUrl ( req . url ! , true )
if ( this . nextConfig . i18n ) {
query . __nextLocale =
query . __nextLocale || this . nextConfig . i18n . defaultLocale
query . __nextDefaultLocale =
query . __nextDefaultLocale || this . nextConfig . i18n . defaultLocale
}
res . statusCode = 404
return this . renderError ( null , req , res , pathname ! , query , setHeaders )
}
protected get _isLikeServerless ( ) : boolean {
return isTargetLikeServerless ( this . nextConfig . target )
}
}
2022-01-14 22:01:35 +01:00
export function prepareServerlessUrl (
req : BaseNextRequest ,
2021-12-05 22:53:11 +01:00
query : ParsedUrlQuery
) : void {
const curUrl = parseUrl ( req . url ! , true )
req . url = formatUrl ( {
. . . curUrl ,
search : undefined ,
query : {
. . . curUrl . query ,
. . . query ,
} ,
} )
}
2022-01-26 07:22:11 +01:00
export { stringifyQuery } from './server-route-utils'
2022-01-14 22:01:35 +01:00
2022-03-28 23:16:43 +02:00
export function isApiRoute ( pathname : string ) {
return pathname === '/api' || pathname . startsWith ( '/api/' )
}
2021-12-05 22:53:11 +01:00
class NoFallbackError extends Error { }
// Internal wrapper around build errors at development
// time, to prevent us from propagating or logging them
export class WrappedBuildError extends Error {
innerError : Error
constructor ( innerError : Error ) {
super ( )
this . innerError = innerError
}
}
type ResponsePayload = {
type : 'html' | 'json'
body : RenderResult
revalidateOptions? : any
}