2020-03-29 00:56:38 +01:00
import compression from 'next/dist/compiled/compression'
2019-05-27 20:20:33 +02:00
import fs from 'fs'
2020-04-05 13:19:14 +02:00
import chalk from 'next/dist/compiled/chalk'
2019-03-01 20:51:13 +01:00
import { IncomingMessage , ServerResponse } from 'http'
2020-03-29 01:30:34 +01:00
import Proxy from 'next/dist/compiled/http-proxy'
2020-03-26 17:58:15 +01:00
import { join , relative , resolve , sep } from 'path'
2018-12-09 22:46:45 +01:00
import { parse as parseQs , ParsedUrlQuery } from 'querystring'
2019-11-11 04:24:53 +01:00
import { format as formatUrl , parse as parseUrl , UrlWithParsedQuery } from 'url'
2020-02-27 13:23:28 +01:00
import { PrerenderManifest } from '../../build'
2020-02-12 02:16:42 +01:00
import {
getRedirectStatus ,
Header ,
Redirect ,
Rewrite ,
RouteType ,
} from '../../lib/check-custom-routes'
2019-09-24 10:50:04 +02:00
import { withCoalescedInvoke } from '../../lib/coalesced-function'
2019-03-01 20:51:13 +01:00
import {
BUILD_ID_FILE ,
2019-05-03 18:57:47 +02:00
CLIENT_PUBLIC_FILES_PATH ,
2019-03-01 20:51:13 +01:00
CLIENT_STATIC_FILES_PATH ,
CLIENT_STATIC_FILES_RUNTIME ,
2019-05-03 18:57:47 +02:00
PAGES_MANIFEST ,
2019-05-27 20:20:33 +02:00
PHASE_PRODUCTION_SERVER ,
2020-02-12 02:16:42 +01:00
PRERENDER_MANIFEST ,
2019-11-11 04:24:53 +01:00
ROUTES_MANIFEST ,
2019-06-10 02:18:38 +02:00
SERVERLESS_DIRECTORY ,
2020-02-12 02:16:42 +01:00
SERVER_DIRECTORY ,
2019-04-24 16:47:50 +02:00
} from '../lib/constants'
2019-05-31 11:27:56 +02:00
import {
getRouteMatcher ,
getRouteRegex ,
getSortedRoutes ,
2019-06-20 20:41:02 +02:00
isDynamicRoute ,
2019-05-31 11:27:56 +02:00
} from '../lib/router/utils'
2018-02-26 12:03:27 +01:00
import * as envConfig from '../lib/runtime-config'
2019-10-23 02:40:33 +02:00
import { isResSent , NextApiRequest , NextApiResponse } from '../lib/utils'
2020-02-12 02:16:42 +01:00
import { apiResolver , tryGetPreviewData , __ApiPreviewProps } from './api-utils'
2019-08-06 00:26:20 +02:00
import loadConfig , { isTargetLikeServerless } from './config'
2019-11-11 04:24:53 +01:00
import pathMatch from './lib/path-match'
2019-05-27 20:20:33 +02:00
import { recursiveReadDirSync } from './lib/recursive-readdir-sync'
2019-07-07 23:45:40 +02:00
import { loadComponents , LoadComponentsReturnType } from './load-components'
2020-02-12 02:16:42 +01:00
import { normalizePagePath } from './normalize-page-path'
2020-03-02 11:58:47 +01:00
import { RenderOpts , RenderOptsPartial , renderToHTML } from './render'
2019-05-26 23:22:43 +02:00
import { getPagePath } from './require'
2019-12-23 22:20:17 +01:00
import Router , {
DynamicRoutes ,
PageChecker ,
2020-02-12 02:16:42 +01:00
Params ,
2020-02-11 00:06:38 +01:00
prepareDestination ,
2020-02-12 02:16:42 +01:00
route ,
Route ,
2019-12-23 22:20:17 +01:00
} from './router'
2019-05-27 20:20:33 +02:00
import { sendHTML } from './send-html'
2020-03-13 10:40:10 +01:00
import { sendPayload } from './send-payload'
2019-05-27 20:20:33 +02:00
import { serveStatic } from './serve-static'
2020-02-07 14:09:06 +01:00
import {
2020-02-12 02:16:42 +01:00
getFallback ,
2020-02-07 14:09:06 +01:00
getSprCache ,
initializeSprCache ,
setSprCache ,
} from './spr-cache'
2020-04-05 13:19:14 +02:00
import { execOnce } from '../lib/utils'
2019-11-13 05:07:19 +01:00
import { isBlockedPage } from './utils'
2020-04-14 18:47:53 +02:00
import { compile as compilePathToRegex } from 'next/dist/compiled/path-to-regexp'
2020-03-31 17:53:50 +02:00
import { loadEnvConfig } from '../../lib/load-env-config'
2020-05-12 21:58:21 +02:00
import './node-polyfill-fetch'
2019-11-09 23:34:53 +01:00
const getCustomRouteMatcher = pathMatch ( true )
2018-12-09 22:46:45 +01:00
type NextConfig = any
2019-07-29 20:35:09 +02:00
type Middleware = (
req : IncomingMessage ,
res : ServerResponse ,
next : ( err? : Error ) = > void
) = > void
2020-02-13 05:28:31 +01:00
type FindComponentsResult = {
components : LoadComponentsReturnType
query : ParsedUrlQuery
}
2019-04-24 16:47:50 +02:00
export type ServerConstructor = {
2019-06-10 16:20:39 +02:00
/ * *
* Where the Next project is located - @default '.'
* /
2019-03-01 20:51:13 +01:00
dir? : string
staticMarkup? : boolean
2019-06-10 16:20:39 +02:00
/ * *
* Hide error messages containing server information - @default false
* /
2019-03-01 20:51:13 +01:00
quiet? : boolean
2019-06-10 16:20:39 +02:00
/ * *
* Object what you would use in next . config . js - @default { }
* /
2019-05-29 13:57:26 +02:00
conf? : NextConfig
2019-09-24 10:50:04 +02:00
dev? : boolean
2020-03-06 17:14:39 +01:00
customServer? : boolean
2018-12-09 22:46:45 +01:00
}
2017-04-07 19:58:35 +02:00
2016-10-06 01:52:50 +02:00
export default class Server {
2018-12-09 22:46:45 +01:00
dir : string
quiet : boolean
nextConfig : NextConfig
distDir : string
2019-09-30 11:08:15 +02:00
pagesDir? : string
2019-05-03 18:57:47 +02:00
publicDir : string
2019-11-13 05:07:19 +01:00
hasStaticDir : boolean
2019-12-10 16:08:42 +01:00
serverBuildDir : string
pagesManifest ? : { [ name : string ] : string }
2018-12-09 22:46:45 +01:00
buildId : string
renderOpts : {
2019-04-15 09:48:14 +02:00
poweredByHeader : boolean
2019-03-01 20:51:13 +01:00
staticMarkup : boolean
buildId : string
generateEtags : boolean
runtimeConfig ? : { [ key : string ] : any }
2019-05-29 13:57:26 +02:00
assetPrefix? : string
canonicalBase : string
dev? : boolean
2020-03-02 11:58:47 +01:00
previewProps : __ApiPreviewProps
2020-03-06 17:14:39 +01:00
customServer? : boolean
2020-03-24 09:31:04 +01:00
ampOptimizerConfig ? : { [ key : string ] : any }
2020-04-14 09:50:39 +02:00
basePath : string
2018-12-09 22:46:45 +01:00
}
2019-07-29 20:35:09 +02:00
private compression? : Middleware
2019-11-16 03:00:24 +01:00
private onErrorMiddleware ? : ( { err } : { err : Error } ) = > Promise < void >
2018-12-09 22:46:45 +01:00
router : Router
2019-12-23 22:20:17 +01:00
protected dynamicRoutes? : DynamicRoutes
2019-11-09 23:34:53 +01:00
protected customRoutes ? : {
rewrites : Rewrite [ ]
redirects : Redirect [ ]
2020-01-01 13:47:58 +01:00
headers : Header [ ]
2019-11-09 23:34:53 +01:00
}
2020-02-24 22:36:59 +01:00
protected staticPathsWorker? : import ( 'jest-worker' ) . default & {
loadStaticPaths : typeof import ( '../../server/static-paths-worker' ) . loadStaticPaths
}
2018-12-09 22:46:45 +01:00
2019-03-01 20:51:13 +01:00
public constructor ( {
dir = '.' ,
staticMarkup = false ,
quiet = false ,
conf = null ,
2019-09-24 10:50:04 +02:00
dev = false ,
2020-03-06 17:14:39 +01:00
customServer = true ,
2019-03-01 20:51:13 +01:00
} : ServerConstructor = { } ) {
2016-10-06 13:05:52 +02:00
this . dir = resolve ( dir )
2016-12-16 21:33:08 +01:00
this . quiet = quiet
2018-09-28 14:05:23 +02:00
const phase = this . currentPhase ( )
2020-03-31 17:53:50 +02:00
loadEnvConfig ( this . dir , dev )
2020-03-26 13:32:41 +01:00
2018-06-04 11:38:46 +02:00
this . nextConfig = loadConfig ( phase , this . dir , conf )
2018-06-13 20:30:55 +02:00
this . distDir = join ( this . dir , this . nextConfig . distDir )
2019-05-03 18:57:47 +02:00
this . publicDir = join ( this . dir , CLIENT_PUBLIC_FILES_PATH )
2019-11-13 05:07:19 +01:00
this . hasStaticDir = fs . existsSync ( join ( this . dir , 'static' ) )
2018-02-05 13:39:32 +01:00
2018-03-13 14:18:59 +01:00
// Only serverRuntimeConfig needs the default
// publicRuntimeConfig gets it's default in client/index.js
2019-03-01 20:51:13 +01:00
const {
serverRuntimeConfig = { } ,
publicRuntimeConfig ,
assetPrefix ,
generateEtags ,
2019-07-29 20:35:09 +02:00
compress ,
2019-03-01 20:51:13 +01:00
} = this . nextConfig
2019-01-29 13:42:07 +01:00
2018-09-28 14:05:23 +02:00
this . buildId = this . readBuildId ( )
2019-05-29 02:32:18 +02:00
2017-03-07 19:43:56 +01:00
this . renderOpts = {
2019-04-15 09:48:14 +02:00
poweredByHeader : this.nextConfig.poweredByHeader ,
2019-05-29 02:32:18 +02:00
canonicalBase : this.nextConfig.amp.canonicalBase ,
2017-03-07 19:43:56 +01:00
staticMarkup ,
2017-04-18 06:18:43 +02:00
buildId : this.buildId ,
2018-12-31 14:44:27 +01:00
generateEtags ,
2020-03-02 11:58:47 +01:00
previewProps : this.getPreviewProps ( ) ,
2020-03-06 17:14:39 +01:00
customServer : customServer === true ? true : undefined ,
2020-03-24 09:31:04 +01:00
ampOptimizerConfig : this.nextConfig.experimental.amp?.optimizer ,
2020-04-14 09:50:39 +02:00
basePath : this.nextConfig.experimental.basePath ,
2017-03-07 19:43:56 +01:00
}
2016-12-16 21:33:08 +01:00
2018-02-27 17:50:14 +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
2019-07-10 16:43:04 +02:00
if ( Object . keys ( publicRuntimeConfig ) . length > 0 ) {
2018-02-27 17:50:14 +01:00
this . renderOpts . runtimeConfig = publicRuntimeConfig
2018-02-26 12:03:27 +01:00
}
2019-08-06 00:26:20 +02:00
if ( compress && this . nextConfig . target === 'server' ) {
2019-07-29 20:35:09 +02:00
this . compression = compression ( ) as Middleware
}
2018-02-27 17:50:14 +01:00
// Initialize next/config with the environment configuration
2020-02-03 23:34:23 +01:00
envConfig . setConfig ( {
serverRuntimeConfig ,
publicRuntimeConfig ,
} )
2018-02-27 17:50:14 +01:00
2019-12-10 16:08:42 +01:00
this . serverBuildDir = join (
this . distDir ,
this . _isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY
)
const pagesManifestPath = join ( this . serverBuildDir , PAGES_MANIFEST )
if ( ! dev ) {
this . pagesManifest = require ( pagesManifestPath )
}
2019-11-09 23:34:53 +01:00
this . router = new Router ( this . generateRoutes ( ) )
2018-02-27 17:50:14 +01:00
this . setAssetPrefix ( assetPrefix )
2019-09-24 10:50:04 +02:00
2019-11-01 20:13:13 +01:00
// call init-server middleware, this is also handled
// individually in serverless bundles when deployed
if ( ! dev && this . nextConfig . experimental . plugins ) {
2019-12-10 16:08:42 +01:00
const initServer = require ( join ( this . serverBuildDir , 'init-server.js' ) )
. default
2019-11-01 20:13:13 +01:00
this . onErrorMiddleware = require ( join (
2019-12-10 16:08:42 +01:00
this . serverBuildDir ,
2019-11-01 20:13:13 +01:00
'on-error-server.js'
) ) . default
initServer ( )
}
2019-09-24 10:50:04 +02:00
initializeSprCache ( {
dev ,
distDir : this.distDir ,
pagesDir : join (
this . distDir ,
this . _isLikeServerless
? SERVERLESS_DIRECTORY
: ` ${ SERVER_DIRECTORY } /static/ ${ this . buildId } ` ,
'pages'
) ,
flushToDisk : this.nextConfig.experimental.sprFlushToDisk ,
} )
2016-12-16 21:33:08 +01:00
}
2016-10-06 01:52:50 +02:00
2019-10-04 18:11:39 +02:00
protected currentPhase ( ) : string {
2019-01-26 02:01:16 +01:00
return PHASE_PRODUCTION_SERVER
2017-07-15 12:29:10 +02:00
}
2019-11-01 20:13:13 +01:00
private logError ( err : Error ) : void {
if ( this . onErrorMiddleware ) {
this . onErrorMiddleware ( { err } )
}
2018-12-31 14:44:27 +01:00
if ( this . quiet ) return
// tslint:disable-next-line
2019-11-01 20:13:13 +01:00
console . error ( err )
2018-12-18 17:12:49 +01:00
}
2020-02-13 05:28:31 +01:00
private async handleRequest (
2019-03-01 20:51:13 +01:00
req : IncomingMessage ,
res : ServerResponse ,
2019-05-29 13:57:26 +02:00
parsedUrl? : UrlWithParsedQuery
2019-03-01 20:51:13 +01:00
) : Promise < void > {
2017-04-07 19:58:35 +02:00
// Parse url if parsedUrl not provided
2017-07-02 07:52:39 +02:00
if ( ! parsedUrl || typeof parsedUrl !== 'object' ) {
2018-12-09 22:46:45 +01:00
const url : any = req . url
parsedUrl = parseUrl ( url , true )
2017-04-07 19:58:35 +02:00
}
2017-02-09 04:22:48 +01:00
2017-04-07 19:58:35 +02:00
// Parse the querystring ourselves if the user doesn't handle querystring parsing
if ( typeof parsedUrl . query === 'string' ) {
parsedUrl . query = parseQs ( parsedUrl . query )
2016-12-16 21:33:08 +01:00
}
2017-04-07 19:58:35 +02:00
2020-04-14 09:50:39 +02:00
const { basePath } = this . nextConfig . experimental
// if basePath is set require it be present
if ( basePath && ! req . url ! . startsWith ( basePath ) ) {
return this . render404 ( req , res , parsedUrl )
} else {
2019-12-29 20:03:12 +01:00
// If replace ends up replacing the full url it'll be `undefined`, meaning we have to default it to `/`
2020-04-14 09:50:39 +02:00
parsedUrl . pathname = parsedUrl . pathname ! . replace ( basePath , '' ) || '/'
req . url = req . url ! . replace ( basePath , '' )
2019-12-29 20:03:12 +01:00
}
2017-06-19 09:27:35 +02:00
res . statusCode = 200
2020-02-13 05:28:31 +01:00
try {
return await this . run ( req , res , parsedUrl )
} catch ( err ) {
2019-03-01 20:51:13 +01:00
this . logError ( err )
res . statusCode = 500
res . end ( 'Internal Server Error' )
2020-02-13 05:28:31 +01:00
}
2017-04-07 19:58:35 +02:00
}
2018-12-31 14:44:27 +01:00
public getRequestHandler() {
2017-04-07 19:58:35 +02:00
return this . handleRequest . bind ( this )
2016-10-06 01:52:50 +02:00
}
2018-12-31 14:44:27 +01:00
public setAssetPrefix ( prefix? : string ) {
2018-02-03 17:12:01 +01:00
this . renderOpts . assetPrefix = prefix ? prefix . replace ( /\/$/ , '' ) : ''
2018-02-02 15:43:36 +01:00
}
2018-10-01 16:31:47 +02:00
// Backwards compatibility
2018-12-31 14:44:27 +01:00
public async prepare ( ) : Promise < void > { }
2016-10-17 09:07:41 +02:00
2018-09-28 14:05:23 +02:00
// Backwards compatibility
2019-10-04 18:11:39 +02:00
protected async close ( ) : Promise < void > { }
2018-09-28 14:05:23 +02:00
2019-10-04 18:11:39 +02:00
protected setImmutableAssetCacheControl ( res : ServerResponse ) {
2018-09-28 14:05:23 +02:00
res . setHeader ( 'Cache-Control' , 'public, max-age=31536000, immutable' )
2016-12-17 05:04:40 +01:00
}
2019-11-09 23:34:53 +01:00
protected getCustomRoutes() {
return require ( join ( this . distDir , ROUTES_MANIFEST ) )
}
2020-02-27 13:23:28 +01:00
private _cachedPreviewManifest : PrerenderManifest | undefined
protected getPrerenderManifest ( ) : PrerenderManifest {
if ( this . _cachedPreviewManifest ) {
return this . _cachedPreviewManifest
2020-02-12 02:16:42 +01:00
}
2020-02-27 13:23:28 +01:00
const manifest = require ( join ( this . distDir , PRERENDER_MANIFEST ) )
return ( this . _cachedPreviewManifest = manifest )
}
protected getPreviewProps ( ) : __ApiPreviewProps {
return this . getPrerenderManifest ( ) . preview
2020-02-12 02:16:42 +01:00
}
2019-12-23 22:20:17 +01:00
protected generateRoutes ( ) : {
2020-02-11 00:06:38 +01:00
headers : Route [ ]
rewrites : Route [ ]
2019-12-23 22:20:17 +01:00
fsRoutes : Route [ ]
2020-02-11 00:06:38 +01:00
redirects : Route [ ]
2019-12-23 22:20:17 +01:00
catchAllRoute : Route
pageChecker : PageChecker
2020-02-11 00:06:38 +01:00
useFileSystemPublicRoutes : boolean
2019-12-23 22:20:17 +01:00
dynamicRoutes : DynamicRoutes | undefined
} {
2019-11-09 23:34:53 +01:00
this . customRoutes = this . getCustomRoutes ( )
2019-10-08 05:12:41 +02:00
const publicRoutes = fs . existsSync ( this . publicDir )
? this . generatePublicRoutes ( )
: [ ]
2019-11-09 23:34:53 +01:00
2019-11-13 05:07:19 +01:00
const staticFilesRoute = this . hasStaticDir
2019-11-05 06:55:35 +01:00
? [
{
// It's very important to keep this route's param optional.
// (but it should support as many params as needed, separated by '/')
// Otherwise this will lead to a pretty simple DOS attack.
// See more: https://github.com/zeit/next.js/issues/2617
match : route ( '/static/:path*' ) ,
2019-11-18 01:12:48 +01:00
name : 'static catchall' ,
2019-11-05 06:55:35 +01:00
fn : async ( req , res , params , parsedUrl ) = > {
2020-03-26 17:58:15 +01:00
const p = join (
this . dir ,
'static' ,
. . . ( params . path || [ ] ) . map ( encodeURIComponent )
)
2019-11-05 06:55:35 +01:00
await this . serveStatic ( req , res , p , parsedUrl )
2019-11-18 01:12:48 +01:00
return {
finished : true ,
}
2019-11-05 06:55:35 +01:00
} ,
} as Route ,
]
: [ ]
2019-10-08 05:12:41 +02:00
2020-02-11 00:06:38 +01:00
let headers : Route [ ] = [ ]
let rewrites : Route [ ] = [ ]
let redirects : Route [ ] = [ ]
2019-12-23 22:20:17 +01:00
const fsRoutes : Route [ ] = [
2018-09-28 14:05:23 +02:00
{
2018-10-01 16:31:47 +02:00
match : route ( '/_next/static/:path*' ) ,
2019-11-18 01:12:48 +01:00
type : 'route' ,
name : '_next/static catchall' ,
2018-12-09 22:46:45 +01:00
fn : async ( req , res , params , parsedUrl ) = > {
2018-09-28 14:05:23 +02:00
// The commons folder holds commonschunk files
// The chunks folder holds dynamic entries
// The buildId folder holds pages and potentially other assets. As buildId changes per build it can be long-term cached.
2019-08-29 22:00:54 +02:00
// make sure to 404 for /_next/static itself
2019-11-18 01:12:48 +01:00
if ( ! params . path ) {
await this . render404 ( req , res , parsedUrl )
return {
finished : true ,
}
}
2019-08-29 22:00:54 +02:00
2019-03-01 20:51:13 +01:00
if (
params . path [ 0 ] === CLIENT_STATIC_FILES_RUNTIME ||
params . path [ 0 ] === 'chunks' ||
2020-01-21 14:19:53 +01:00
params . path [ 0 ] === 'css' ||
params . path [ 0 ] === 'media' ||
2019-03-01 20:51:13 +01:00
params . path [ 0 ] === this . buildId
) {
2018-09-28 14:05:23 +02:00
this . setImmutableAssetCacheControl ( res )
2018-05-19 21:43:18 +02:00
}
2019-03-01 20:51:13 +01:00
const p = join (
this . distDir ,
CLIENT_STATIC_FILES_PATH ,
2019-05-29 13:57:26 +02:00
. . . ( params . path || [ ] )
2019-03-01 20:51:13 +01:00
)
2018-12-09 22:46:45 +01:00
await this . serveStatic ( req , res , p , parsedUrl )
2019-11-18 01:12:48 +01:00
return {
finished : true ,
}
2018-12-31 14:44:27 +01:00
} ,
2017-04-03 20:10:24 +02:00
} ,
2019-09-24 10:50:04 +02:00
{
match : route ( '/_next/data/:path*' ) ,
2019-11-18 01:12:48 +01:00
type : 'route' ,
name : '_next/data catchall' ,
2019-09-24 10:50:04 +02:00
fn : async ( req , res , params , _parsedUrl ) = > {
2019-10-10 19:07:51 +02:00
// 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 ) {
2019-11-18 01:12:48 +01:00
await this . render404 ( req , res , _parsedUrl )
return {
finished : true ,
}
2019-10-10 19:07:51 +02:00
}
// remove buildId from URL
params . path . shift ( )
// show 404 if it doesn't end with .json
if ( ! params . path [ params . path . length - 1 ] . endsWith ( '.json' ) ) {
2019-11-18 01:12:48 +01:00
await this . render404 ( req , res , _parsedUrl )
return {
finished : true ,
}
2019-10-10 19:07:51 +02:00
}
// re-create page's pathname
const pathname = ` / ${ params . path . join ( '/' ) } `
. replace ( /\.json$/ , '' )
. replace ( /\/index$/ , '/' )
2019-09-24 10:50:04 +02:00
const parsedUrl = parseUrl ( pathname , true )
await this . render (
req ,
res ,
pathname ,
2020-01-27 23:50:59 +01:00
{ . . . _parsedUrl . query , _nextDataReq : '1' } ,
2019-09-24 10:50:04 +02:00
parsedUrl
)
2019-11-18 01:12:48 +01:00
return {
finished : true ,
}
2019-09-24 10:50:04 +02:00
} ,
} ,
2018-09-28 14:05:23 +02:00
{
2018-10-01 16:31:47 +02:00
match : route ( '/_next/:path*' ) ,
2019-11-18 01:12:48 +01:00
type : 'route' ,
name : '_next catchall' ,
2018-09-28 14:05:23 +02:00
// This path is needed because `render()` does a check for `/_next` and the calls the routing again
2018-12-09 22:46:45 +01:00
fn : async ( req , res , _params , parsedUrl ) = > {
2018-09-28 14:05:23 +02:00
await this . render404 ( req , res , parsedUrl )
2019-11-18 01:12:48 +01:00
return {
finished : true ,
}
2019-05-11 13:18:56 +02:00
} ,
} ,
2019-11-20 16:53:31 +01:00
. . . publicRoutes ,
. . . staticFilesRoute ,
2018-09-28 14:05:23 +02:00
]
2018-09-07 14:38:01 +02:00
2019-11-09 23:34:53 +01:00
if ( this . customRoutes ) {
const getCustomRoute = (
2020-01-01 13:47:58 +01:00
r : Rewrite | Redirect | Header ,
type : RouteType
2020-02-11 00:06:38 +01:00
) = >
( {
. . . r ,
type ,
match : getCustomRouteMatcher ( r . source ) ,
name : type ,
fn : async ( req , res , params , parsedUrl ) = > ( { finished : false } ) ,
} as Route & Rewrite & Header )
2019-11-09 23:34:53 +01:00
2020-04-30 20:14:32 +02:00
const updateHeaderValue = ( value : string , params : Params ) : string = > {
if ( ! value . includes ( ':' ) ) {
return value
}
const { parsedDestination } = prepareDestination ( value , params , { } )
if (
! parsedDestination . pathname ||
! parsedDestination . pathname . startsWith ( '/' )
) {
return compilePathToRegex ( value , { validate : false } ) ( params )
}
return formatUrl ( parsedDestination )
}
2020-01-01 13:47:58 +01:00
// Headers come very first
2020-02-11 00:06:38 +01:00
headers = this . customRoutes . headers . map ( r = > {
const route = getCustomRoute ( r , 'header' )
return {
match : route.match ,
type : route . type ,
name : ` ${ route . type } ${ route . source } header route ` ,
2020-04-14 18:47:53 +02:00
fn : async ( _req , res , params , _parsedUrl ) = > {
2020-04-30 20:14:32 +02:00
const hasParams = Object . keys ( params ) . length > 0
2020-02-11 00:06:38 +01:00
for ( const header of ( route as Header ) . headers ) {
2020-04-14 18:47:53 +02:00
let { key , value } = header
2020-04-30 20:14:32 +02:00
if ( hasParams ) {
key = updateHeaderValue ( key , params )
value = updateHeaderValue ( value , params )
2020-04-14 18:47:53 +02:00
}
res . setHeader ( key , value )
2020-02-11 00:06:38 +01:00
}
return { finished : false }
} ,
} as Route
} )
2019-11-09 23:34:53 +01:00
2020-02-11 00:06:38 +01:00
redirects = this . customRoutes . redirects . map ( redirect = > {
const route = getCustomRoute ( redirect , 'redirect' )
return {
type : route . type ,
match : route.match ,
statusCode : route.statusCode ,
name : ` Redirect route ` ,
2020-04-11 12:57:52 +02:00
fn : async ( _req , res , params , parsedUrl ) = > {
2020-02-11 00:06:38 +01:00
const { parsedDestination } = prepareDestination (
route . destination ,
2020-03-10 21:09:35 +01:00
params ,
2020-04-30 20:14:32 +02:00
parsedUrl . query
2020-02-11 00:06:38 +01:00
)
const updatedDestination = formatUrl ( parsedDestination )
res . setHeader ( 'Location' , updatedDestination )
res . statusCode = getRedirectStatus ( route as Redirect )
// Since IE11 doesn't support the 308 header add backwards
// compatibility using refresh header
if ( res . statusCode === 308 ) {
res . setHeader ( 'Refresh' , ` 0;url= ${ updatedDestination } ` )
}
2019-12-31 21:13:55 +01:00
2020-02-11 00:06:38 +01:00
res . end ( )
return {
finished : true ,
}
} ,
} as Route
} )
2019-11-18 01:12:48 +01:00
2020-02-11 00:06:38 +01:00
rewrites = this . customRoutes . rewrites . map ( rewrite = > {
const route = getCustomRoute ( rewrite , 'rewrite' )
return {
check : true ,
type : route . type ,
name : ` Rewrite route ` ,
match : route.match ,
2020-04-11 12:57:52 +02:00
fn : async ( req , res , params , parsedUrl ) = > {
2020-02-11 00:06:38 +01:00
const { newUrl , parsedDestination } = prepareDestination (
route . destination ,
2020-04-11 12:57:52 +02:00
params ,
2020-04-30 20:14:32 +02:00
parsedUrl . query ,
true
2020-02-11 00:06:38 +01:00
)
// external rewrite, proxy it
if ( parsedDestination . protocol ) {
const target = formatUrl ( parsedDestination )
const proxy = new Proxy ( {
target ,
changeOrigin : true ,
ignorePath : true ,
2020-02-04 20:08:03 +01:00
} )
2020-02-11 00:06:38 +01:00
proxy . web ( req , res )
2020-01-10 21:54:23 +01:00
2020-02-11 00:06:38 +01:00
proxy . on ( 'error' , ( err : Error ) = > {
console . error ( ` Error occurred proxying ${ target } ` , err )
} )
2019-11-18 01:12:48 +01:00
return {
2020-02-11 00:06:38 +01:00
finished : true ,
2019-11-09 23:34:53 +01:00
}
2020-02-11 00:06:38 +01:00
}
; ( req as any ) . _nextDidRewrite = true
2019-12-23 22:20:17 +01:00
2020-02-11 00:06:38 +01:00
return {
finished : false ,
pathname : newUrl ,
query : parsedDestination.query ,
}
} ,
} as Route
} )
2020-01-20 22:13:47 +01:00
}
const catchAllRoute : Route = {
match : route ( '/:path*' ) ,
type : 'route' ,
name : 'Catchall render' ,
fn : async ( req , res , params , parsedUrl ) = > {
const { pathname , query } = parsedUrl
if ( ! pathname ) {
throw new Error ( 'pathname is undefined' )
}
2020-01-08 17:30:53 +01:00
if ( params ? . path ? . [ 0 ] === 'api' ) {
2019-12-23 22:20:17 +01:00
const handled = await this . handleApiRequest (
req as NextApiRequest ,
res as NextApiResponse ,
2020-01-23 10:23:34 +01:00
pathname ! ,
query
2019-12-23 22:20:17 +01:00
)
if ( handled ) {
return { finished : true }
}
}
await this . render ( req , res , pathname , query , parsedUrl )
2019-11-20 16:53:31 +01:00
return {
finished : true ,
}
} ,
2019-12-23 22:20:17 +01:00
}
2019-11-18 01:12:48 +01:00
2020-02-11 00:06:38 +01:00
const { useFileSystemPublicRoutes } = this . nextConfig
2019-05-27 20:20:33 +02:00
2020-02-11 00:06:38 +01:00
if ( useFileSystemPublicRoutes ) {
this . dynamicRoutes = this . getDynamicRoutes ( )
2017-01-12 16:38:43 +01:00
}
2016-10-06 01:52:50 +02:00
2019-12-23 22:20:17 +01:00
return {
2020-02-11 00:06:38 +01:00
headers ,
2019-12-23 22:20:17 +01:00
fsRoutes ,
2020-02-11 00:06:38 +01:00
rewrites ,
redirects ,
2019-12-23 22:20:17 +01:00
catchAllRoute ,
2020-02-11 00:06:38 +01:00
useFileSystemPublicRoutes ,
2019-12-23 22:20:17 +01:00
dynamicRoutes : this.dynamicRoutes ,
pageChecker : this.hasPage.bind ( this ) ,
}
2018-09-28 14:05:23 +02:00
}
2019-12-10 16:08:42 +01:00
private async getPagePath ( pathname : string ) {
return getPagePath (
pathname ,
this . distDir ,
this . _isLikeServerless ,
this . renderOpts . dev
)
}
protected async hasPage ( pathname : string ) : Promise < boolean > {
let found = false
try {
found = ! ! ( await this . getPagePath ( pathname ) )
} catch ( _ ) { }
return found
}
2019-11-18 01:12:48 +01:00
protected async _beforeCatchAllRender (
_req : IncomingMessage ,
_res : ServerResponse ,
_params : Params ,
_parsedUrl : UrlWithParsedQuery
) {
return false
}
2019-12-10 16:08:42 +01:00
// Used to build API page in development
protected async ensureApiPage ( pathname : string ) { }
2019-05-11 13:18:56 +02:00
/ * *
* Resolves ` API ` request , in development builds on demand
* @param req http request
* @param res http response
* @param pathname path of request
* /
2019-05-26 23:22:43 +02:00
private async handleApiRequest (
2019-12-10 16:08:42 +01:00
req : IncomingMessage ,
res : ServerResponse ,
2020-01-23 10:23:34 +01:00
pathname : string ,
query : ParsedUrlQuery
2019-05-26 23:22:43 +02:00
) {
2019-12-10 16:08:42 +01:00
let page = pathname
2019-06-27 18:01:36 +02:00
let params : Params | boolean = false
2019-12-10 16:08:42 +01:00
let pageFound = await this . hasPage ( page )
2019-07-09 00:50:01 +02:00
2019-12-10 16:08:42 +01:00
if ( ! pageFound && this . dynamicRoutes ) {
2019-06-27 18:01:36 +02:00
for ( const dynamicRoute of this . dynamicRoutes ) {
params = dynamicRoute . match ( pathname )
2020-02-01 15:14:50 +01:00
if ( dynamicRoute . page . startsWith ( '/api' ) && params ) {
2019-12-10 16:08:42 +01:00
page = dynamicRoute . page
pageFound = true
2019-06-27 18:01:36 +02:00
break
}
}
}
2019-12-10 16:08:42 +01:00
if ( ! pageFound ) {
2019-12-23 22:20:17 +01:00
return false
2019-07-09 00:50:01 +02:00
}
2019-12-10 16:08:42 +01:00
// Make sure the page is built before getting the path
// or else it won't be in the manifest yet
await this . ensureApiPage ( page )
const builtPagePath = await this . getPagePath ( page )
const pageModule = require ( builtPagePath )
2020-01-23 10:23:34 +01:00
query = { . . . query , . . . params }
2019-07-09 00:50:01 +02:00
2019-08-06 00:26:20 +02:00
if ( ! this . renderOpts . dev && this . _isLikeServerless ) {
2019-12-10 16:08:42 +01:00
if ( typeof pageModule . default === 'function' ) {
2020-02-28 17:36:19 +01:00
prepareServerlessUrl ( req , query )
2019-12-23 22:20:17 +01:00
await pageModule . default ( req , res )
return true
2019-07-09 00:50:01 +02:00
}
}
2020-02-12 02:16:42 +01:00
await apiResolver (
req ,
res ,
query ,
pageModule ,
2020-03-02 11:58:47 +01:00
this . renderOpts . previewProps ,
2020-02-12 02:16:42 +01:00
this . onErrorMiddleware
)
2019-12-23 22:20:17 +01:00
return true
2019-05-11 13:18:56 +02:00
}
2019-10-04 18:11:39 +02:00
protected generatePublicRoutes ( ) : Route [ ] {
2020-01-10 03:56:05 +01:00
const publicFiles = new Set (
recursiveReadDirSync ( this . publicDir ) . map ( p = > p . replace ( /\\/g , '/' ) )
)
return [
{
match : route ( '/:path*' ) ,
name : 'public folder catchall' ,
fn : async ( req , res , params , parsedUrl ) = > {
2020-03-26 17:58:15 +01:00
const pathParts : string [ ] = params . path || [ ]
const path = ` / ${ pathParts . join ( '/' ) } `
2020-01-10 03:56:05 +01:00
if ( publicFiles . has ( path ) ) {
await this . serveStatic (
req ,
res ,
// we need to re-encode it since send decodes it
2020-03-26 17:58:15 +01:00
join ( this . publicDir , . . . pathParts . map ( encodeURIComponent ) ) ,
2020-01-10 03:56:05 +01:00
parsedUrl
)
2019-11-18 01:12:48 +01:00
return {
finished : true ,
}
2020-01-10 03:56:05 +01:00
}
return {
finished : false ,
}
} ,
} as Route ,
]
2019-05-03 18:57:47 +02:00
}
2019-10-04 18:11:39 +02:00
protected getDynamicRoutes() {
2019-12-10 16:08:42 +01:00
const dynamicRoutedPages = Object . keys ( this . pagesManifest ! ) . filter (
isDynamicRoute
)
2019-05-30 22:42:45 +02:00
return getSortedRoutes ( dynamicRoutedPages ) . map ( page = > ( {
page ,
match : getRouteMatcher ( getRouteRegex ( page ) ) ,
} ) )
2019-05-27 20:20:33 +02:00
}
2019-07-29 20:35:09 +02:00
private handleCompression ( req : IncomingMessage , res : ServerResponse ) {
if ( this . compression ) {
this . compression ( req , res , ( ) = > { } )
}
}
2019-10-04 18:11:39 +02:00
protected async run (
2019-03-01 20:51:13 +01:00
req : IncomingMessage ,
res : ServerResponse ,
2019-05-29 13:57:26 +02:00
parsedUrl : UrlWithParsedQuery
2019-03-01 20:51:13 +01:00
) {
2019-07-29 20:35:09 +02:00
this . handleCompression ( req , res )
2018-11-04 01:22:33 +01:00
try {
2019-11-18 01:12:48 +01:00
const matched = await this . router . execute ( req , res , parsedUrl )
if ( matched ) {
2018-11-04 01:22:33 +01:00
return
}
} catch ( err ) {
if ( err . code === 'DECODE_FAILED' ) {
res . statusCode = 400
return this . renderError ( null , req , res , '/_error' , { } )
}
throw err
2017-01-12 16:38:43 +01:00
}
2019-07-04 10:22:25 +02:00
await this . render404 ( req , res , parsedUrl )
2016-10-06 01:52:50 +02:00
}
2019-10-04 18:11:39 +02:00
protected async sendHTML (
2019-03-01 20:51:13 +01:00
req : IncomingMessage ,
res : ServerResponse ,
2019-05-29 13:57:26 +02:00
html : string
2019-03-01 20:51:13 +01:00
) {
2019-04-15 09:48:14 +02:00
const { generateEtags , poweredByHeader } = this . renderOpts
return sendHTML ( req , res , html , { generateEtags , poweredByHeader } )
2018-12-09 22:46:45 +01:00
}
2019-03-01 20:51:13 +01:00
public async render (
req : IncomingMessage ,
res : ServerResponse ,
pathname : string ,
query : ParsedUrlQuery = { } ,
2019-05-29 13:57:26 +02:00
parsedUrl? : UrlWithParsedQuery
2019-03-01 20:51:13 +01:00
) : Promise < void > {
2020-04-06 18:54:42 +02:00
if ( ! pathname . startsWith ( '/' ) ) {
console . warn (
` Cannot render page with path " ${ pathname } ", did you mean "/ ${ pathname } "?. See more info here: https://err.sh/next.js/render-no-starting-slash `
)
}
2018-12-09 22:46:45 +01:00
const url : any = req . url
2019-11-13 05:07:19 +01:00
2020-04-06 20:55:22 +02:00
// 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
2019-11-13 05:07:19 +01:00
if (
2020-04-06 20:55:22 +02:00
! query . _nextDataReq &&
( url . match ( /^\/_next\// ) ||
( this . hasStaticDir && url . match ( /^\/static\// ) ) )
2019-11-13 05:07:19 +01:00
) {
2017-04-07 19:58:35 +02:00
return this . handleRequest ( req , res , parsedUrl )
}
2018-11-18 20:44:50 +01:00
if ( isBlockedPage ( pathname ) ) {
2018-12-09 22:46:45 +01:00
return this . render404 ( req , res , parsedUrl )
2017-07-06 14:29:25 +02:00
}
2020-03-01 18:26:31 +01:00
const html = await this . renderToHTML ( req , res , pathname , query )
2018-12-09 22:46:45 +01:00
// Request was ended by the user
if ( html === null ) {
2018-02-01 23:25:30 +01:00
return
}
2018-12-09 22:46:45 +01:00
return this . sendHTML ( req , res , html )
2016-12-16 21:33:08 +01:00
}
2016-10-19 14:41:45 +02:00
2019-05-26 23:22:43 +02:00
private async findPageComponents (
2019-03-01 20:51:13 +01:00
pathname : string ,
2020-02-13 05:28:31 +01:00
query : ParsedUrlQuery = { } ,
params : Params | null = null
) : Promise < FindComponentsResult | null > {
const paths = [
// try serving a static AMP version first
query . amp ? normalizePagePath ( pathname ) + '.amp' : null ,
pathname ,
] . filter ( Boolean )
for ( const pagePath of paths ) {
2019-05-22 18:36:53 +02:00
try {
2020-02-13 05:28:31 +01:00
const components = await loadComponents (
2019-05-26 23:22:43 +02:00
this . distDir ,
this . buildId ,
2020-02-13 05:28:31 +01:00
pagePath ! ,
! this . renderOpts . dev && this . _isLikeServerless
2019-05-26 23:22:43 +02:00
)
2020-02-13 05:28:31 +01:00
return {
components ,
query : {
2020-02-27 18:57:39 +01:00
. . . ( components . getStaticProps
2020-03-20 09:46:52 +01:00
? { _nextDataReq : query._nextDataReq , amp : query.amp }
2020-02-13 05:28:31 +01:00
: query ) ,
. . . ( params || { } ) ,
} ,
}
2019-05-22 18:36:53 +02:00
} catch ( err ) {
if ( err . code !== 'ENOENT' ) throw err
}
}
2020-02-13 05:28:31 +01:00
return null
2019-05-26 23:22:43 +02:00
}
2020-02-28 17:56:50 +01:00
private async getStaticPaths (
pathname : string
) : Promise < {
staticPaths : string [ ] | undefined
hasStaticFallback : boolean
} > {
// we lazy load the staticPaths to prevent the user
// from waiting on them for the page to load in dev mode
let staticPaths : string [ ] | undefined
let hasStaticFallback = false
if ( ! this . renderOpts . dev ) {
// `staticPaths` is intentionally set to `undefined` as it should've
// been caught when checking disk data.
staticPaths = undefined
// Read whether or not fallback should exist from the manifest.
hasStaticFallback =
typeof this . getPrerenderManifest ( ) . dynamicRoutes [ pathname ] . fallback ===
'string'
} else {
const __getStaticPaths = async ( ) = > {
const paths = await this . staticPathsWorker ! . loadStaticPaths (
this . distDir ,
this . buildId ,
pathname ,
! this . renderOpts . dev && this . _isLikeServerless
)
return paths
}
; ( { paths : staticPaths , fallback : hasStaticFallback } = (
await withCoalescedInvoke ( __getStaticPaths ) (
` staticPaths- ${ pathname } ` ,
[ ]
)
) . value )
}
return { staticPaths , hasStaticFallback }
}
2019-05-26 23:22:43 +02:00
private async renderToHTMLWithComponents (
req : IncomingMessage ,
res : ServerResponse ,
pathname : string ,
2020-02-13 05:28:31 +01:00
{ components , query } : FindComponentsResult ,
2020-03-02 11:58:47 +01:00
opts : RenderOptsPartial
2020-03-03 19:25:45 +01:00
) : Promise < string | null > {
2020-02-01 15:47:42 +01:00
// we need to ensure the status code if /404 is visited directly
2020-02-19 20:54:38 +01:00
if ( pathname === '/404' ) {
2020-02-01 15:47:42 +01:00
res . statusCode = 404
}
2019-05-22 18:36:53 +02:00
// handle static page
2020-02-13 05:28:31 +01:00
if ( typeof components . Component === 'string' ) {
return components . Component
2019-05-26 23:22:43 +02:00
}
2019-09-24 10:50:04 +02:00
// check request state
const isLikeServerless =
2020-02-13 05:28:31 +01:00
typeof components . Component === 'object' &&
typeof ( components . Component as any ) . renderReqToHTML === 'function'
2020-02-27 18:57:39 +01:00
const isSSG = ! ! components . getStaticProps
const isServerProps = ! ! components . getServerSideProps
const hasStaticPaths = ! ! components . getStaticPaths
2020-01-27 23:50:59 +01:00
2020-03-20 09:46:52 +01:00
if ( ! query . amp ) {
delete query . amp
}
2020-01-27 23:50:59 +01:00
// Toggle whether or not this is a Data request
2020-03-02 11:58:47 +01:00
const isDataReq = ! ! query . _nextDataReq
2020-01-27 23:50:59 +01:00
delete query . _nextDataReq
2020-03-04 13:58:12 +01:00
let previewData : string | false | object | undefined
let isPreviewMode = false
if ( isServerProps || isSSG ) {
previewData = tryGetPreviewData ( req , res , this . renderOpts . previewProps )
isPreviewMode = previewData !== false
}
2019-09-24 10:50:04 +02:00
// non-spr requests should render like normal
2020-01-15 02:22:15 +01:00
if ( ! isSSG ) {
2019-09-24 10:50:04 +02:00
// handle serverless
if ( isLikeServerless ) {
2020-01-27 23:50:59 +01:00
if ( isDataReq ) {
2020-02-13 05:28:31 +01:00
const renderResult = await ( components . Component as any ) . renderReqToHTML (
2020-01-27 23:50:59 +01:00
req ,
res ,
2020-03-09 18:30:44 +01:00
'passthrough'
2020-01-27 23:50:59 +01:00
)
2020-02-28 17:36:19 +01:00
sendPayload (
2020-01-27 23:50:59 +01:00
res ,
JSON . stringify ( renderResult ? . renderOpts ? . pageData ) ,
2020-03-13 10:40:10 +01:00
'json' ,
2020-02-28 17:36:19 +01:00
! this . renderOpts . dev
? {
2020-03-13 10:40:10 +01:00
private : isPreviewMode ,
stateful : true , // non-SSG data request
2020-02-28 17:36:19 +01:00
}
: undefined
2020-01-27 23:50:59 +01:00
)
return null
}
2020-02-28 17:36:19 +01:00
prepareServerlessUrl ( req , query )
2020-02-13 05:28:31 +01:00
return ( components . Component as any ) . renderReqToHTML ( req , res )
2019-09-24 10:50:04 +02:00
}
2020-01-27 23:50:59 +01:00
if ( isDataReq && isServerProps ) {
const props = await renderToHTML ( req , res , pathname , query , {
2020-02-13 05:28:31 +01:00
. . . components ,
2020-01-27 23:50:59 +01:00
. . . opts ,
isDataReq ,
} )
2020-02-28 17:36:19 +01:00
sendPayload (
res ,
JSON . stringify ( props ) ,
2020-03-13 10:40:10 +01:00
'json' ,
2020-02-28 17:36:19 +01:00
! this . renderOpts . dev
? {
2020-03-13 10:40:10 +01:00
private : isPreviewMode ,
stateful : true , // GSSP data request
2020-02-28 17:36:19 +01:00
}
: undefined
)
2020-01-27 23:50:59 +01:00
return null
}
2020-03-04 13:58:12 +01:00
const html = await renderToHTML ( req , res , pathname , query , {
2020-02-13 05:28:31 +01:00
. . . components ,
2019-09-24 10:50:04 +02:00
. . . opts ,
} )
2020-03-13 10:40:10 +01:00
if ( html && isServerProps ) {
sendPayload ( res , html , 'html' , {
2020-03-04 13:58:12 +01:00
private : isPreviewMode ,
2020-03-13 10:40:10 +01:00
stateful : true , // GSSP request
2020-03-04 13:58:12 +01:00
} )
2020-03-13 10:40:10 +01:00
return null
2020-03-04 13:58:12 +01:00
}
return html
}
2020-02-12 02:16:42 +01:00
2020-04-06 20:55:22 +02:00
// Compute the iSSG cache key
let urlPathname = ` ${ parseUrl ( req . url || '' ) . pathname ! } ${
2020-03-20 09:46:52 +01:00
query . amp ? '.amp' : ''
} `
2020-04-06 20:55:22 +02:00
// remove /_next/data prefix from urlPathname so it matches
// for direct page visit and /_next/data visit
if ( isDataReq && urlPathname . includes ( this . buildId ) ) {
urlPathname = ( urlPathname . split ( this . buildId ) . pop ( ) || '/' )
. replace ( /\.json$/ , '' )
. replace ( /\/index$/ , '/' )
}
2020-02-12 02:16:42 +01:00
const ssgCacheKey = isPreviewMode
2020-05-04 22:57:45 +02:00
? undefined // Preview mode bypasses the cache
2020-02-24 22:36:59 +01:00
: urlPathname
2019-09-24 10:50:04 +02:00
// Complete the response with cached data if its present
2020-05-04 22:57:45 +02:00
const cachedData = ssgCacheKey ? await getSprCache ( ssgCacheKey ) : undefined
2019-09-24 10:50:04 +02:00
if ( cachedData ) {
2020-01-15 02:22:15 +01:00
const data = isDataReq
2019-09-24 10:50:04 +02:00
? JSON . stringify ( cachedData . pageData )
: cachedData . html
2020-02-28 17:36:19 +01:00
sendPayload (
2019-09-24 10:50:04 +02:00
res ,
data ,
2020-03-13 10:40:10 +01:00
isDataReq ? 'json' : 'html' ,
! this . renderOpts . dev
? {
private : isPreviewMode ,
stateful : false , // GSP response
revalidate :
cachedData . curRevalidate !== undefined
? cachedData . curRevalidate
: /* default to minimum revalidate (this should be an invariant) */ 1 ,
}
2020-02-22 10:26:39 +01:00
: undefined
2019-09-24 10:50:04 +02:00
)
// Stop the request chain here if the data we sent was up-to-date
if ( ! cachedData . isStale ) {
return null
}
2019-05-22 18:36:53 +02:00
}
2019-05-26 23:22:43 +02:00
2019-09-24 10:50:04 +02:00
// If we're here, that means data is missing or it's stale.
2020-05-04 22:57:45 +02:00
const maybeCoalesceInvoke = ssgCacheKey
? ( fn : any ) = > withCoalescedInvoke ( fn ) . bind ( null , ssgCacheKey , [ ] )
: ( fn : any ) = > async ( ) = > {
const value = await fn ( )
return { isOrigin : true , value }
}
2019-09-24 10:50:04 +02:00
2020-05-04 22:57:45 +02:00
const doRender = maybeCoalesceInvoke ( async function ( ) : Promise < {
2019-09-24 10:50:04 +02:00
html : string | null
2020-01-15 02:22:15 +01:00
pageData : any
2019-09-24 10:50:04 +02:00
sprRevalidate : number | false
} > {
2020-01-15 02:22:15 +01:00
let pageData : any
2019-09-24 10:50:04 +02:00
let html : string | null
let sprRevalidate : number | false
let renderResult
// handle serverless
if ( isLikeServerless ) {
2020-02-13 05:28:31 +01:00
renderResult = await ( components . Component as any ) . renderReqToHTML (
2020-01-25 05:25:11 +01:00
req ,
res ,
2020-03-09 18:30:44 +01:00
'passthrough'
2020-01-25 05:25:11 +01:00
)
2019-09-24 10:50:04 +02:00
html = renderResult . html
2020-01-15 02:22:15 +01:00
pageData = renderResult . renderOpts . pageData
2019-09-24 10:50:04 +02:00
sprRevalidate = renderResult . renderOpts . revalidate
} else {
2020-03-02 11:58:47 +01:00
const renderOpts : RenderOpts = {
2020-02-13 05:28:31 +01:00
. . . components ,
2019-09-24 10:50:04 +02:00
. . . opts ,
}
renderResult = await renderToHTML ( req , res , pathname , query , renderOpts )
html = renderResult
2020-03-02 11:58:47 +01:00
// TODO: change this to a different passing mechanism
pageData = ( renderOpts as any ) . pageData
sprRevalidate = ( renderOpts as any ) . revalidate
2019-09-24 10:50:04 +02:00
}
2020-01-15 02:22:15 +01:00
return { html , pageData , sprRevalidate }
2019-06-10 02:16:14 +02:00
} )
2019-09-24 10:50:04 +02:00
2020-02-13 02:06:07 +01:00
const isProduction = ! this . renderOpts . dev
2020-02-12 02:16:42 +01:00
const isDynamicPathname = isDynamicRoute ( pathname )
2020-02-13 02:06:07 +01:00
const didRespond = isResSent ( res )
2020-02-24 22:36:59 +01:00
2020-02-28 17:56:50 +01:00
const { staticPaths , hasStaticFallback } = hasStaticPaths
? await this . getStaticPaths ( pathname )
: { staticPaths : undefined , hasStaticFallback : false }
2020-02-24 22:36:59 +01:00
2020-02-13 02:06:07 +01:00
// const isForcedBlocking =
// req.headers['X-Prerender-Bypass-Mode'] !== 'Blocking'
// When we did not respond from cache, we need to choose to block on
// rendering or return a skeleton.
//
// * Data requests always block.
//
// * Preview mode toggles all pages to be resolved in a blocking manner.
//
2020-02-24 22:36:59 +01:00
// * Non-dynamic pages should block (though this is an impossible
2020-02-13 02:06:07 +01:00
// case in production).
//
2020-02-24 22:36:59 +01:00
// * Dynamic pages should return their skeleton if not defined in
// getStaticPaths, then finish the data request on the client-side.
2020-02-13 02:06:07 +01:00
//
2020-02-12 02:16:42 +01:00
if (
2020-02-13 02:06:07 +01:00
! didRespond &&
2020-02-12 02:16:42 +01:00
! isDataReq &&
2020-02-13 02:06:07 +01:00
! isPreviewMode &&
isDynamicPathname &&
2020-02-24 22:36:59 +01:00
// Development should trigger fallback when the path is not in
// `getStaticPaths`
( isProduction || ! staticPaths || ! staticPaths . includes ( urlPathname ) )
2020-02-12 02:16:42 +01:00
) {
2020-02-27 13:23:28 +01:00
if (
// In development, fall through to render to handle missing
// getStaticPaths.
( isProduction || staticPaths ) &&
// When fallback isn't present, abort this render so we 404
! hasStaticFallback
) {
2020-03-03 19:25:45 +01:00
throw new NoFallbackError ( )
2020-02-27 13:23:28 +01:00
}
2020-02-13 02:06:07 +01:00
let html : string
2020-02-07 14:09:06 +01:00
2020-02-13 02:06:07 +01:00
// Production already emitted the fallback as static HTML.
if ( isProduction ) {
2020-02-07 14:09:06 +01:00
html = await getFallback ( pathname )
2020-02-13 02:06:07 +01:00
}
// We need to generate the fallback on-demand for development.
else {
2020-02-07 14:09:06 +01:00
query . __nextFallback = 'true'
if ( isLikeServerless ) {
2020-02-28 17:36:19 +01:00
prepareServerlessUrl ( req , query )
2020-03-13 10:40:10 +01:00
const renderResult = await ( components . Component as any ) . renderReqToHTML (
req ,
res ,
'passthrough'
)
html = renderResult . html
2020-02-07 14:09:06 +01:00
} else {
html = ( await renderToHTML ( req , res , pathname , query , {
2020-02-13 05:28:31 +01:00
. . . components ,
2020-02-07 14:09:06 +01:00
. . . opts ,
} ) ) as string
}
}
2020-03-13 10:40:10 +01:00
sendPayload ( res , html , 'html' )
2020-02-07 14:09:06 +01:00
}
2020-02-13 05:28:31 +01:00
const {
isOrigin ,
value : { html , pageData , sprRevalidate } ,
2020-05-04 22:57:45 +02:00
} = await doRender ( )
2020-02-13 05:28:31 +01:00
if ( ! isResSent ( res ) ) {
2020-02-28 17:36:19 +01:00
sendPayload (
2020-02-13 05:28:31 +01:00
res ,
isDataReq ? JSON . stringify ( pageData ) : html ,
2020-03-13 10:40:10 +01:00
isDataReq ? 'json' : 'html' ,
2020-02-28 17:36:19 +01:00
! this . renderOpts . dev
2020-03-13 10:40:10 +01:00
? {
private : isPreviewMode ,
stateful : false , // GSP response
revalidate : sprRevalidate ,
}
2020-02-28 17:36:19 +01:00
: undefined
2020-02-13 05:28:31 +01:00
)
}
2019-09-24 10:50:04 +02:00
2020-05-04 22:57:45 +02:00
// Update the SPR cache if the head request and cacheable
if ( isOrigin && ssgCacheKey ) {
await setSprCache ( ssgCacheKey , { html : html ! , pageData } , sprRevalidate )
2020-02-13 05:28:31 +01:00
}
return null
2018-12-18 17:12:49 +01:00
}
2020-02-13 05:28:31 +01:00
public async renderToHTML (
2019-03-01 20:51:13 +01:00
req : IncomingMessage ,
res : ServerResponse ,
pathname : string ,
2020-03-01 18:26:31 +01:00
query : ParsedUrlQuery = { }
2019-03-01 20:51:13 +01:00
) : Promise < string | null > {
2020-02-13 05:28:31 +01:00
try {
const result = await this . findPageComponents ( pathname , query )
if ( result ) {
2020-03-03 19:25:45 +01:00
try {
return await this . renderToHTMLWithComponents (
req ,
res ,
pathname ,
result ,
{ . . . this . renderOpts }
)
} catch ( err ) {
if ( ! ( err instanceof NoFallbackError ) ) {
throw err
}
2020-02-27 13:23:28 +01:00
}
2020-02-13 05:28:31 +01:00
}
2019-05-27 20:20:33 +02:00
2020-02-13 05:28:31 +01:00
if ( this . dynamicRoutes ) {
for ( const dynamicRoute of this . dynamicRoutes ) {
const params = dynamicRoute . match ( pathname )
if ( ! params ) {
continue
}
2019-05-27 20:20:33 +02:00
2020-02-13 05:28:31 +01:00
const result = await this . findPageComponents (
dynamicRoute . page ,
query ,
params
)
if ( result ) {
2020-03-03 19:25:45 +01:00
try {
return await this . renderToHTMLWithComponents (
req ,
res ,
dynamicRoute . page ,
result ,
{ . . . this . renderOpts , params }
)
} catch ( err ) {
if ( ! ( err instanceof NoFallbackError ) ) {
throw err
}
2020-02-27 13:23:28 +01:00
}
2019-05-27 20:20:33 +02:00
}
}
2020-02-13 05:28:31 +01:00
}
} catch ( err ) {
this . logError ( err )
res . statusCode = 500
return await this . renderErrorToHTML ( err , req , res , pathname , query )
}
res . statusCode = 404
return await this . renderErrorToHTML ( null , req , res , pathname , query )
2016-11-24 15:03:16 +01:00
}
2019-03-01 20:51:13 +01:00
public async renderError (
err : Error | null ,
req : IncomingMessage ,
res : ServerResponse ,
pathname : string ,
2019-05-29 13:57:26 +02:00
query : ParsedUrlQuery = { }
2019-03-01 20:51:13 +01:00
) : Promise < void > {
res . setHeader (
'Cache-Control' ,
2019-05-29 13:57:26 +02:00
'no-cache, no-store, max-age=0, must-revalidate'
2019-03-01 20:51:13 +01:00
)
2016-12-16 21:33:08 +01:00
const html = await this . renderErrorToHTML ( err , req , res , pathname , query )
2018-12-31 14:44:27 +01:00
if ( html === null ) {
2018-12-13 01:00:46 +01:00
return
}
2018-12-09 22:46:45 +01:00
return this . sendHTML ( req , res , html )
2016-10-06 01:52:50 +02:00
}
2020-04-05 13:19:14 +02:00
private customErrorNo404Warn = execOnce ( ( ) = > {
console . warn (
chalk . bold . yellow ( ` Warning: ` ) +
chalk . yellow (
` 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://err.sh/next.js/custom-error-no-custom-404 `
)
)
} )
2019-03-01 20:51:13 +01:00
public async renderErrorToHTML (
err : Error | null ,
req : IncomingMessage ,
res : ServerResponse ,
_pathname : string ,
2019-05-29 13:57:26 +02:00
query : ParsedUrlQuery = { }
2019-03-01 20:51:13 +01:00
) {
2020-02-13 05:28:31 +01:00
let result : null | FindComponentsResult = null
2020-01-20 15:10:24 +01:00
2020-02-01 15:47:42 +01:00
const is404 = res . statusCode === 404
let using404Page = false
2020-01-20 15:10:24 +01:00
// use static 404 page if available and is 404 response
2020-02-01 15:47:42 +01:00
if ( is404 ) {
2020-02-19 20:54:38 +01:00
result = await this . findPageComponents ( '/404' )
using404Page = result !== null
2020-01-20 15:10:24 +01:00
}
if ( ! result ) {
result = await this . findPageComponents ( '/_error' , query )
}
2020-04-05 13:19:14 +02:00
if (
process . env . NODE_ENV !== 'production' &&
! using404Page &&
2020-04-21 14:22:39 +02:00
( await this . hasPage ( '/_error' ) ) &&
! ( await this . hasPage ( '/404' ) )
2020-04-05 13:19:14 +02:00
) {
this . customErrorNo404Warn ( )
}
2020-02-27 13:23:28 +01:00
let html : string | null
2019-06-19 18:26:22 +02:00
try {
2020-03-03 19:25:45 +01:00
try {
html = await this . renderToHTMLWithComponents (
req ,
res ,
using404Page ? '/404' : '/_error' ,
result ! ,
{
. . . this . renderOpts ,
err ,
}
)
} catch ( err ) {
if ( err instanceof NoFallbackError ) {
throw new Error ( 'invariant: failed to render error page' )
2019-06-19 18:26:22 +02:00
}
2020-03-03 19:25:45 +01:00
throw err
2020-02-27 13:23:28 +01:00
}
2019-06-19 18:26:22 +02:00
} catch ( err ) {
console . error ( err )
res . statusCode = 500
html = 'Internal Server Error'
}
return html
2016-11-24 15:03:16 +01:00
}
2019-03-01 20:51:13 +01:00
public async render404 (
req : IncomingMessage ,
res : ServerResponse ,
2019-05-29 13:57:26 +02:00
parsedUrl? : UrlWithParsedQuery
2019-03-01 20:51:13 +01:00
) : Promise < void > {
2018-12-09 22:46:45 +01:00
const url : any = req . url
const { pathname , query } = parsedUrl ? parsedUrl : parseUrl ( url , true )
2016-12-16 21:33:08 +01:00
res . statusCode = 404
2020-02-19 20:54:38 +01:00
return this . renderError ( null , req , res , pathname ! , query )
2016-12-16 21:33:08 +01:00
}
2016-11-03 16:12:37 +01:00
2019-03-01 20:51:13 +01:00
public async serveStatic (
req : IncomingMessage ,
res : ServerResponse ,
path : string ,
2019-05-29 13:57:26 +02:00
parsedUrl? : UrlWithParsedQuery
2019-03-01 20:51:13 +01:00
) : Promise < void > {
2017-06-01 02:16:32 +02:00
if ( ! this . isServeableUrl ( path ) ) {
2018-12-09 22:46:45 +01:00
return this . render404 ( req , res , parsedUrl )
2017-06-01 02:16:32 +02:00
}
2019-07-04 10:22:25 +02:00
if ( ! ( req . method === 'GET' || req . method === 'HEAD' ) ) {
res . statusCode = 405
res . setHeader ( 'Allow' , [ 'GET' , 'HEAD' ] )
return this . renderError ( null , req , res , path )
}
2017-01-01 20:36:37 +01:00
try {
2018-12-09 22:46:45 +01:00
await serveStatic ( req , res , path )
2017-01-01 20:36:37 +01:00
} catch ( err ) {
2018-11-30 17:09:23 +01:00
if ( err . code === 'ENOENT' || err . statusCode === 404 ) {
2018-12-09 22:46:45 +01:00
this . render404 ( req , res , parsedUrl )
2019-07-16 13:30:29 +02:00
} else if ( err . statusCode === 412 ) {
res . statusCode = 412
return this . renderError ( err , req , res , path )
2017-01-01 20:36:37 +01:00
} else {
throw err
}
}
}
2020-03-26 17:58:15 +01:00
private _validFilesystemPathSet : Set < string > | null = null
private getFilesystemPaths ( ) : Set < string > {
if ( this . _validFilesystemPathSet ) {
return this . _validFilesystemPathSet
}
const pathUserFilesStatic = join ( this . dir , 'static' )
let userFilesStatic : string [ ] = [ ]
if ( this . hasStaticDir && fs . existsSync ( pathUserFilesStatic ) ) {
userFilesStatic = recursiveReadDirSync ( pathUserFilesStatic ) . map ( f = >
join ( '.' , 'static' , f )
)
}
let userFilesPublic : string [ ] = [ ]
if ( this . publicDir && fs . existsSync ( this . publicDir ) ) {
userFilesPublic = recursiveReadDirSync ( this . publicDir ) . map ( f = >
join ( '.' , 'public' , f )
)
}
let nextFilesStatic : string [ ] = [ ]
nextFilesStatic = recursiveReadDirSync (
join ( this . distDir , 'static' )
) . map ( f = > join ( '.' , relative ( this . dir , this . distDir ) , 'static' , f ) )
return ( this . _validFilesystemPathSet = new Set < string > ( [
. . . nextFilesStatic ,
. . . userFilesPublic ,
. . . userFilesStatic ,
] ) )
}
protected isServeableUrl ( untrustedFileUrl : string ) : boolean {
// This method mimics what the version of `send` we use does:
// 1. decodeURIComponent:
// https://github.com/pillarjs/send/blob/0.17.1/index.js#L989
// https://github.com/pillarjs/send/blob/0.17.1/index.js#L518-L522
// 2. resolve:
// https://github.com/pillarjs/send/blob/de073ed3237ade9ff71c61673a34474b30e5d45b/index.js#L561
let decodedUntrustedFilePath : string
try {
// (1) Decode the URL so we have the proper file name
decodedUntrustedFilePath = decodeURIComponent ( untrustedFileUrl )
} catch {
return false
}
// (2) Resolve "up paths" to determine real request
const untrustedFilePath = resolve ( decodedUntrustedFilePath )
// don't allow null bytes anywhere in the file path
if ( untrustedFilePath . indexOf ( '\0' ) !== - 1 ) {
return false
}
// Check if .next/static, static and public are in the path.
// If not the path is not available.
2017-06-01 02:16:32 +02:00
if (
2020-03-26 17:58:15 +01:00
( untrustedFilePath . startsWith ( join ( this . distDir , 'static' ) + sep ) ||
untrustedFilePath . startsWith ( join ( this . dir , 'static' ) + sep ) ||
untrustedFilePath . startsWith ( join ( this . dir , 'public' ) + sep ) ) === false
2017-06-01 02:16:32 +02:00
) {
return false
}
2020-03-26 17:58:15 +01:00
// Check against the real filesystem paths
const filesystemUrls = this . getFilesystemPaths ( )
const resolved = relative ( this . dir , untrustedFilePath )
return filesystemUrls . has ( resolved )
2017-06-01 02:16:32 +02:00
}
2019-10-04 18:11:39 +02:00
protected readBuildId ( ) : string {
2018-12-06 16:46:53 +01:00
const buildIdFile = join ( this . distDir , BUILD_ID_FILE )
try {
return fs . readFileSync ( buildIdFile , 'utf8' ) . trim ( )
} catch ( err ) {
if ( ! fs . existsSync ( buildIdFile ) ) {
2019-03-01 20:51:13 +01:00
throw new Error (
2019-11-11 04:24:53 +01:00
` Could not find a valid build in the ' ${ this . distDir } ' directory! Try building your app with 'next build' before starting the server. `
2019-03-01 20:51:13 +01:00
)
2018-12-06 16:46:53 +01:00
}
throw err
2018-07-25 13:45:42 +02:00
}
2017-01-11 21:16:18 +01:00
}
2019-08-06 00:26:20 +02:00
private get _isLikeServerless ( ) : boolean {
return isTargetLikeServerless ( this . nextConfig . target )
}
2017-02-21 00:48:17 +01:00
}
2020-02-28 17:36:19 +01:00
function prepareServerlessUrl ( req : IncomingMessage , query : ParsedUrlQuery ) {
const curUrl = parseUrl ( req . url ! , true )
req . url = formatUrl ( {
. . . curUrl ,
search : undefined ,
query : {
. . . curUrl . query ,
. . . query ,
} ,
} )
}
2020-03-03 19:25:45 +01:00
class NoFallbackError extends Error { }