2018-12-09 21:46:45 +00:00
/* eslint-disable import/first */
import { IncomingMessage , ServerResponse } from 'http'
2017-06-01 00:16:32 +00:00
import { resolve , join , sep } from 'path'
2018-12-09 21:46:45 +00:00
import { parse as parseUrl , UrlWithParsedQuery } from 'url'
import { parse as parseQs , ParsedUrlQuery } from 'querystring'
2017-03-07 18:43:56 +00:00
import fs from 'fs'
2018-12-13 00:00:46 +00:00
import { renderToHTML } from './render'
2018-12-07 14:52:29 +00:00
import { sendHTML } from './send-html'
2018-12-06 15:54:33 +00:00
import { serveStatic } from './serve-static'
2018-12-09 21:46:45 +00:00
import Router , { route , Route } from './router'
2018-11-18 19:44:50 +00:00
import { isInternalUrl , isBlockedPage } from './utils'
2018-10-01 22:55:31 +00:00
import loadConfig from 'next-server/next-config'
2019-01-26 01:01:16 +00:00
import { PHASE_PRODUCTION_SERVER , BUILD_ID_FILE , CLIENT_STATIC_FILES_PATH , CLIENT_STATIC_FILES_RUNTIME } from 'next-server/constants'
2018-02-26 11:03:27 +00:00
import * as envConfig from '../lib/runtime-config'
2018-12-18 16:12:49 +00:00
import { loadComponents } from './load-components'
2018-12-09 21:46:45 +00:00
type NextConfig = any
type ServerConstructor = {
dir? : string ,
staticMarkup? : boolean ,
quiet? : boolean ,
2018-12-31 13:44:27 +00:00
conf? : NextConfig ,
2018-12-09 21:46:45 +00:00
}
2017-04-07 17:58:35 +00:00
2016-10-05 23:52:50 +00:00
export default class Server {
2018-12-09 21:46:45 +00:00
dir : string
quiet : boolean
nextConfig : NextConfig
distDir : string
buildId : string
renderOpts : {
2019-02-14 15:22:57 +00:00
ampEnabled : boolean ,
2018-12-09 21:46:45 +00:00
staticMarkup : boolean ,
buildId : string ,
generateEtags : boolean ,
runtimeConfig ? : { [ key : string ] : any } ,
2018-12-31 13:44:27 +00:00
assetPrefix? : string ,
2018-12-09 21:46:45 +00:00
}
router : Router
2018-12-31 13:44:27 +00:00
public constructor ( { dir = '.' , staticMarkup = false , quiet = false , conf = null } : ServerConstructor = { } ) {
2016-10-06 11:05:52 +00:00
this . dir = resolve ( dir )
2016-12-16 20:33:08 +00:00
this . quiet = quiet
2018-09-28 12:05:23 +00:00
const phase = this . currentPhase ( )
2018-06-04 09:38:46 +00:00
this . nextConfig = loadConfig ( phase , this . dir , conf )
2018-06-13 18:30:55 +00:00
this . distDir = join ( this . dir , this . nextConfig . distDir )
2018-02-05 12:39:32 +00:00
2018-03-13 13:18:59 +00:00
// Only serverRuntimeConfig needs the default
// publicRuntimeConfig gets it's default in client/index.js
2019-01-29 12:42:07 +00:00
const { serverRuntimeConfig = { } , publicRuntimeConfig , assetPrefix , generateEtags , target } = this . nextConfig
if ( process . env . NODE_ENV === 'production' && target !== 'server' ) throw new Error ( 'Cannot start server when target is not server. https://err.sh/zeit/next.js/next-start-serverless' )
2018-03-13 13:18:59 +00:00
2018-09-28 12:05:23 +00:00
this . buildId = this . readBuildId ( )
2017-03-07 18:43:56 +00:00
this . renderOpts = {
2019-02-14 15:22:57 +00:00
ampEnabled : this.nextConfig.experimental.amp ,
2017-03-07 18:43:56 +00:00
staticMarkup ,
2017-04-18 04:18:43 +00:00
buildId : this.buildId ,
2018-12-31 13:44:27 +00:00
generateEtags ,
2017-03-07 18:43:56 +00:00
}
2016-12-16 20:33:08 +00:00
2018-02-27 16:50:14 +00: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 ( publicRuntimeConfig ) {
this . renderOpts . runtimeConfig = publicRuntimeConfig
2018-02-26 11:03:27 +00:00
}
2018-02-27 16:50:14 +00:00
// Initialize next/config with the environment configuration
envConfig . setConfig ( {
serverRuntimeConfig ,
2018-12-31 13:44:27 +00:00
publicRuntimeConfig ,
2018-02-27 16:50:14 +00:00
} )
2018-10-01 14:31:47 +00:00
const routes = this . generateRoutes ( )
this . router = new Router ( routes )
2018-02-27 16:50:14 +00:00
this . setAssetPrefix ( assetPrefix )
2016-12-16 20:33:08 +00:00
}
2016-10-05 23:52:50 +00:00
2018-12-31 13:44:27 +00:00
private currentPhase ( ) : string {
2019-01-26 01:01:16 +00:00
return PHASE_PRODUCTION_SERVER
2017-07-15 10:29:10 +00:00
}
2018-12-18 16:12:49 +00:00
private logError ( . . . args : any ) : void {
2018-12-31 13:44:27 +00:00
if ( this . quiet ) return
// tslint:disable-next-line
2018-12-18 16:12:49 +00:00
console . error ( . . . args )
}
2018-12-31 13:44:27 +00:00
private handleRequest ( req : IncomingMessage , res : ServerResponse , parsedUrl? : UrlWithParsedQuery ) : Promise < void > {
2017-04-07 17:58:35 +00:00
// Parse url if parsedUrl not provided
2017-07-02 05:52:39 +00:00
if ( ! parsedUrl || typeof parsedUrl !== 'object' ) {
2018-12-09 21:46:45 +00:00
const url : any = req . url
parsedUrl = parseUrl ( url , true )
2017-04-07 17:58:35 +00:00
}
2017-02-09 03:22:48 +00:00
2017-04-07 17:58:35 +00: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 20:33:08 +00:00
}
2017-04-07 17:58:35 +00:00
2017-06-19 07:27:35 +00:00
res . statusCode = 200
2017-04-07 17:58:35 +00:00
return this . run ( req , res , parsedUrl )
2017-06-19 07:27:35 +00:00
. catch ( ( err ) = > {
2018-12-18 16:12:49 +00:00
this . logError ( err )
2017-06-19 07:27:35 +00:00
res . statusCode = 500
2018-09-27 19:10:53 +00:00
res . end ( 'Internal Server Error' )
2017-06-19 07:27:35 +00:00
} )
2017-04-07 17:58:35 +00:00
}
2018-12-31 13:44:27 +00:00
public getRequestHandler() {
2017-04-07 17:58:35 +00:00
return this . handleRequest . bind ( this )
2016-10-05 23:52:50 +00:00
}
2018-12-31 13:44:27 +00:00
public setAssetPrefix ( prefix? : string ) {
2018-02-03 16:12:01 +00:00
this . renderOpts . assetPrefix = prefix ? prefix . replace ( /\/$/ , '' ) : ''
2018-02-02 14:43:36 +00:00
}
2018-10-01 14:31:47 +00:00
// Backwards compatibility
2018-12-31 13:44:27 +00:00
public async prepare ( ) : Promise < void > { }
2016-10-17 07:07:41 +00:00
2018-09-28 12:05:23 +00:00
// Backwards compatibility
2018-12-31 13:44:27 +00:00
private async close ( ) : Promise < void > { }
2018-09-28 12:05:23 +00:00
2018-12-31 13:44:27 +00:00
private setImmutableAssetCacheControl ( res : ServerResponse ) {
2018-09-28 12:05:23 +00:00
res . setHeader ( 'Cache-Control' , 'public, max-age=31536000, immutable' )
2016-12-17 04:04:40 +00:00
}
2018-12-31 13:44:27 +00:00
private generateRoutes ( ) : Route [ ] {
2018-12-09 21:46:45 +00:00
const routes : Route [ ] = [
2018-09-28 12:05:23 +00:00
{
2018-10-01 14:31:47 +00:00
match : route ( '/_next/static/:path*' ) ,
2018-12-09 21:46:45 +00:00
fn : async ( req , res , params , parsedUrl ) = > {
2018-09-28 12:05:23 +00: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.
if ( params . path [ 0 ] === CLIENT_STATIC_FILES_RUNTIME || params . path [ 0 ] === 'chunks' || params . path [ 0 ] === this . buildId ) {
this . setImmutableAssetCacheControl ( res )
2018-05-19 19:43:18 +00:00
}
2018-09-28 12:05:23 +00:00
const p = join ( this . distDir , CLIENT_STATIC_FILES_PATH , . . . ( params . path || [ ] ) )
2018-12-09 21:46:45 +00:00
await this . serveStatic ( req , res , p , parsedUrl )
2018-12-31 13:44:27 +00:00
} ,
2017-04-03 18:10:24 +00:00
} ,
2018-09-28 12:05:23 +00:00
{
2018-10-01 14:31:47 +00:00
match : route ( '/_next/:path*' ) ,
2018-09-28 12:05:23 +00:00
// This path is needed because `render()` does a check for `/_next` and the calls the routing again
2018-12-09 21:46:45 +00:00
fn : async ( req , res , _params , parsedUrl ) = > {
2018-09-28 12:05:23 +00:00
await this . render404 ( req , res , parsedUrl )
2018-12-31 13:44:27 +00:00
} ,
2018-09-28 12:05:23 +00:00
} ,
{
2018-11-14 08:55:25 +00: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.
2018-09-28 12:05:23 +00:00
// See more: https://github.com/zeit/next.js/issues/2617
2018-10-01 14:31:47 +00:00
match : route ( '/static/:path*' ) ,
2018-12-09 21:46:45 +00:00
fn : async ( req , res , params , parsedUrl ) = > {
2018-09-28 12:05:23 +00:00
const p = join ( this . dir , 'static' , . . . ( params . path || [ ] ) )
2018-12-09 21:46:45 +00:00
await this . serveStatic ( req , res , p , parsedUrl )
2018-12-31 13:44:27 +00:00
} ,
} ,
2018-09-28 12:05:23 +00:00
]
2018-09-07 12:38:01 +00:00
2018-09-09 20:32:23 +00:00
if ( this . nextConfig . useFileSystemPublicRoutes ) {
2019-02-14 15:22:57 +00:00
if ( this . nextConfig . experimental . amp ) {
// 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
routes . push ( {
match : route ( '/:path*/amp' ) ,
fn : async ( req , res , params , parsedUrl ) = > {
let pathname
if ( ! params . path ) {
pathname = '/'
} else {
pathname = '/' + params . path . join ( '/' )
}
const { query } = parsedUrl
if ( ! pathname ) {
throw new Error ( 'pathname is undefined' )
}
await this . renderToAMP ( req , res , pathname , query , parsedUrl )
} ,
} )
}
2018-11-14 08:55:25 +00: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.
2018-07-24 09:24:40 +00:00
// See more: https://github.com/zeit/next.js/issues/2617
2018-09-28 12:05:23 +00:00
routes . push ( {
2018-10-01 14:31:47 +00:00
match : route ( '/:path*' ) ,
2018-12-09 21:46:45 +00:00
fn : async ( req , res , _params , parsedUrl ) = > {
2018-09-28 12:05:23 +00:00
const { pathname , query } = parsedUrl
2018-12-31 13:44:27 +00:00
if ( ! pathname ) {
2018-12-09 21:46:45 +00:00
throw new Error ( 'pathname is undefined' )
}
2018-09-28 12:05:23 +00:00
await this . render ( req , res , pathname , query , parsedUrl )
2018-12-31 13:44:27 +00:00
} ,
2018-09-28 12:05:23 +00:00
} )
2017-01-12 15:38:43 +00:00
}
2016-10-05 23:52:50 +00:00
2018-09-28 12:05:23 +00:00
return routes
}
2018-12-31 13:44:27 +00:00
private async run ( req : IncomingMessage , res : ServerResponse , parsedUrl : UrlWithParsedQuery ) {
2018-11-04 00:22:33 +00:00
try {
const fn = this . router . match ( req , res , parsedUrl )
if ( fn ) {
await fn ( )
return
}
} catch ( err ) {
if ( err . code === 'DECODE_FAILED' ) {
res . statusCode = 400
return this . renderError ( null , req , res , '/_error' , { } )
}
throw err
2017-01-12 15:38:43 +00:00
}
if ( req . method === 'GET' || req . method === 'HEAD' ) {
2017-02-02 06:51:08 +00:00
await this . render404 ( req , res , parsedUrl )
2017-01-12 15:38:43 +00:00
} else {
res . statusCode = 501
2018-09-27 19:10:53 +00:00
res . end ( 'Not Implemented' )
2016-10-05 23:52:50 +00:00
}
}
2018-12-09 21:46:45 +00:00
private async sendHTML ( req : IncomingMessage , res : ServerResponse , html : string ) {
const { generateEtags } = this . renderOpts
return sendHTML ( req , res , html , { generateEtags } )
}
2018-12-31 13:44:27 +00:00
public async render ( req : IncomingMessage , res : ServerResponse , pathname : string , query : ParsedUrlQuery = { } , parsedUrl? : UrlWithParsedQuery ) : Promise < void > {
2018-12-09 21:46:45 +00:00
const url : any = req . url
if ( isInternalUrl ( url ) ) {
2017-04-07 17:58:35 +00:00
return this . handleRequest ( req , res , parsedUrl )
}
2018-11-18 19:44:50 +00:00
if ( isBlockedPage ( pathname ) ) {
2018-12-09 21:46:45 +00:00
return this . render404 ( req , res , parsedUrl )
2017-07-06 12:29:25 +00:00
}
2016-12-16 20:33:08 +00:00
const html = await this . renderToHTML ( req , res , pathname , query )
2018-12-09 21:46:45 +00:00
// Request was ended by the user
if ( html === null ) {
2018-02-01 22:25:30 +00:00
return
}
2018-02-26 11:03:27 +00:00
if ( this . nextConfig . poweredByHeader ) {
2019-02-15 16:49:40 +00:00
res . setHeader ( 'X-Powered-By' , 'Next.js ' + process . env . __NEXT_VERSION )
2018-02-14 17:02:48 +00:00
}
2018-12-09 21:46:45 +00:00
return this . sendHTML ( req , res , html )
2016-12-16 20:33:08 +00:00
}
2016-10-19 12:41:45 +00:00
2019-02-14 15:22:57 +00:00
public async renderToAMP ( req : IncomingMessage , res : ServerResponse , pathname : string , query : ParsedUrlQuery = { } , parsedUrl? : UrlWithParsedQuery ) : Promise < void > {
if ( ! this . nextConfig . experimental . amp ) {
throw new Error ( '"experimental.amp" is not enabled in "next.config.js"' )
}
const url : any = req . url
if ( isInternalUrl ( url ) ) {
return this . handleRequest ( req , res , parsedUrl )
}
if ( isBlockedPage ( pathname ) ) {
return this . render404 ( req , res , parsedUrl )
}
const html = await this . renderToAMPHTML ( req , res , pathname , query )
// Request was ended by the user
if ( html === null ) {
return
}
if ( this . nextConfig . poweredByHeader ) {
res . setHeader ( 'X-Powered-By' , 'Next.js ' + process . env . NEXT_VERSION )
}
return this . sendHTML ( req , res , html )
}
2018-12-18 16:12:49 +00:00
private async renderToHTMLWithComponents ( req : IncomingMessage , res : ServerResponse , pathname : string , query : ParsedUrlQuery = { } , opts : any ) {
const result = await loadComponents ( this . distDir , this . buildId , pathname )
return renderToHTML ( req , res , pathname , query , { . . . result , . . . opts } )
}
2019-02-14 15:22:57 +00:00
public async renderToAMPHTML ( req : IncomingMessage , res : ServerResponse , pathname : string , query : ParsedUrlQuery = { } ) : Promise < string | null > {
if ( ! this . nextConfig . experimental . amp ) {
throw new Error ( '"experimental.amp" is not enabled in "next.config.js"' )
}
return this . renderToHTML ( req , res , pathname , query , { amphtml : true } )
}
public async renderToHTML ( req : IncomingMessage , res : ServerResponse , pathname : string , query : ParsedUrlQuery = { } , { amphtml } : { amphtml? : boolean } = { } ) : Promise < string | null > {
2016-11-24 14:03:16 +00:00
try {
2018-12-09 21:46:45 +00:00
// To make sure the try/catch is executed
2019-02-14 15:22:57 +00:00
const html = await this . renderToHTMLWithComponents ( req , res , pathname , query , { . . . this . renderOpts , amphtml } )
2018-12-09 21:46:45 +00:00
return html
2016-11-24 14:03:16 +00:00
} catch ( err ) {
2016-12-16 20:33:08 +00:00
if ( err . code === 'ENOENT' ) {
2018-12-13 18:46:16 +00:00
res . statusCode = 404
2016-12-16 20:33:08 +00:00
return this . renderErrorToHTML ( null , req , res , pathname , query )
} else {
2018-12-18 16:12:49 +00:00
this . logError ( err )
2016-12-16 20:33:08 +00:00
res . statusCode = 500
return this . renderErrorToHTML ( err , req , res , pathname , query )
2016-10-05 23:52:50 +00:00
}
}
2016-11-24 14:03:16 +00:00
}
2018-12-31 13:44:27 +00:00
public async renderError ( err : Error | null , req : IncomingMessage , res : ServerResponse , pathname : string , query : ParsedUrlQuery = { } ) : Promise < void > {
2018-11-04 00:22:33 +00:00
res . setHeader ( 'Cache-Control' , 'no-cache, no-store, max-age=0, must-revalidate' )
2016-12-16 20:33:08 +00:00
const html = await this . renderErrorToHTML ( err , req , res , pathname , query )
2018-12-31 13:44:27 +00:00
if ( html === null ) {
2018-12-13 00:00:46 +00:00
return
}
2018-12-09 21:46:45 +00:00
return this . sendHTML ( req , res , html )
2016-10-05 23:52:50 +00:00
}
2018-12-31 13:44:27 +00:00
public async renderErrorToHTML ( err : Error | null , req : IncomingMessage , res : ServerResponse , _pathname : string , query : ParsedUrlQuery = { } ) {
2018-12-18 16:12:49 +00:00
return this . renderToHTMLWithComponents ( req , res , '/_error' , query , { . . . this . renderOpts , err } )
2016-11-24 14:03:16 +00:00
}
2018-12-31 13:44:27 +00:00
public async render404 ( req : IncomingMessage , res : ServerResponse , parsedUrl? : UrlWithParsedQuery ) : Promise < void > {
2018-12-09 21:46:45 +00:00
const url : any = req . url
const { pathname , query } = parsedUrl ? parsedUrl : parseUrl ( url , true )
2018-12-31 13:44:27 +00:00
if ( ! pathname ) {
2018-12-09 21:46:45 +00:00
throw new Error ( 'pathname is undefined' )
}
2016-12-16 20:33:08 +00:00
res . statusCode = 404
2017-02-16 02:48:35 +00:00
return this . renderError ( null , req , res , pathname , query )
2016-12-16 20:33:08 +00:00
}
2016-11-03 15:12:37 +00:00
2018-12-31 13:44:27 +00:00
public async serveStatic ( req : IncomingMessage , res : ServerResponse , path : string , parsedUrl? : UrlWithParsedQuery ) : Promise < void > {
2017-06-01 00:16:32 +00:00
if ( ! this . isServeableUrl ( path ) ) {
2018-12-09 21:46:45 +00:00
return this . render404 ( req , res , parsedUrl )
2017-06-01 00:16:32 +00:00
}
2017-01-01 19:36:37 +00:00
try {
2018-12-09 21:46:45 +00:00
await serveStatic ( req , res , path )
2017-01-01 19:36:37 +00:00
} catch ( err ) {
2018-11-30 16:09:23 +00:00
if ( err . code === 'ENOENT' || err . statusCode === 404 ) {
2018-12-09 21:46:45 +00:00
this . render404 ( req , res , parsedUrl )
2017-01-01 19:36:37 +00:00
} else {
throw err
}
}
}
2018-12-31 13:44:27 +00:00
private isServeableUrl ( path : string ) : boolean {
2017-06-01 00:16:32 +00:00
const resolved = resolve ( path )
if (
2018-06-04 13:45:39 +00:00
resolved . indexOf ( join ( this . distDir ) + sep ) !== 0 &&
2017-06-01 00:16:32 +00:00
resolved . indexOf ( join ( this . dir , 'static' ) + sep ) !== 0
) {
// Seems like the user is trying to traverse the filesystem.
return false
}
return true
}
2018-12-31 13:44:27 +00:00
private readBuildId ( ) : string {
2018-12-06 15:46:53 +00:00
const buildIdFile = join ( this . distDir , BUILD_ID_FILE )
try {
return fs . readFileSync ( buildIdFile , 'utf8' ) . trim ( )
} catch ( err ) {
if ( ! fs . existsSync ( buildIdFile ) ) {
throw new Error ( ` Could not find a valid build in the ' ${ this . distDir } ' directory! Try building your app with 'next build' before starting the server. ` )
}
throw err
2018-07-25 11:45:42 +00:00
}
2017-01-11 20:16:18 +00:00
}
2017-02-20 23:48:17 +00:00
}