next-server.ts 31.7 KB
Newer Older
1
import compression from 'compression'
J
Joe Haddad 已提交
2
import fs from 'fs'
J
Joe Haddad 已提交
3
import { IncomingMessage, ServerResponse } from 'http'
J
Joe Haddad 已提交
4
import { join, resolve, sep } from 'path'
J
Joe Haddad 已提交
5
import { compile as compilePathToRegex } from 'path-to-regexp'
6
import { parse as parseQs, ParsedUrlQuery } from 'querystring'
7
import { format as formatUrl, parse as parseUrl, UrlWithParsedQuery } from 'url'
J
Joe Haddad 已提交
8

J
JJ Kasper 已提交
9
import { withCoalescedInvoke } from '../../lib/coalesced-function'
J
Joe Haddad 已提交
10 11
import {
  BUILD_ID_FILE,
12
  CLIENT_PUBLIC_FILES_PATH,
J
Joe Haddad 已提交
13 14
  CLIENT_STATIC_FILES_PATH,
  CLIENT_STATIC_FILES_RUNTIME,
15
  PAGES_MANIFEST,
J
Joe Haddad 已提交
16
  PHASE_PRODUCTION_SERVER,
17
  ROUTES_MANIFEST,
J
Joe Haddad 已提交
18
  SERVER_DIRECTORY,
19
  SERVERLESS_DIRECTORY,
20
  DEFAULT_REDIRECT_STATUS,
T
Tim Neutkens 已提交
21
} from '../lib/constants'
J
Joe Haddad 已提交
22 23 24 25
import {
  getRouteMatcher,
  getRouteRegex,
  getSortedRoutes,
26
  isDynamicRoute,
J
Joe Haddad 已提交
27
} from '../lib/router/utils'
28
import * as envConfig from '../lib/runtime-config'
J
Joe Haddad 已提交
29
import { isResSent, NextApiRequest, NextApiResponse } from '../lib/utils'
30
import { apiResolver } from './api-utils'
31
import loadConfig, { isTargetLikeServerless } from './config'
32
import pathMatch from './lib/path-match'
J
Joe Haddad 已提交
33
import { recursiveReadDirSync } from './lib/recursive-readdir-sync'
34
import { loadComponents, LoadComponentsReturnType } from './load-components'
J
Joe Haddad 已提交
35
import { renderToHTML } from './render'
J
Joe Haddad 已提交
36
import { getPagePath } from './require'
37 38 39 40 41 42 43
import Router, {
  Params,
  route,
  Route,
  DynamicRoutes,
  PageChecker,
} from './router'
J
Joe Haddad 已提交
44 45
import { sendHTML } from './send-html'
import { serveStatic } from './serve-static'
J
Joe Haddad 已提交
46
import { getSprCache, initializeSprCache, setSprCache } from './spr-cache'
47
import { isBlockedPage } from './utils'
48 49 50 51 52 53
import {
  Redirect,
  Rewrite,
  RouteType,
  Header,
} from '../../lib/check-custom-routes'
J
JJ Kasper 已提交
54 55

const getCustomRouteMatcher = pathMatch(true)
56 57 58

type NextConfig = any

59 60 61 62 63 64
type Middleware = (
  req: IncomingMessage,
  res: ServerResponse,
  next: (err?: Error) => void
) => void

T
Tim Neutkens 已提交
65
export type ServerConstructor = {
66 67 68
  /**
   * Where the Next project is located - @default '.'
   */
J
Joe Haddad 已提交
69 70
  dir?: string
  staticMarkup?: boolean
71 72 73
  /**
   * Hide error messages containing server information - @default false
   */
J
Joe Haddad 已提交
74
  quiet?: boolean
75 76 77
  /**
   * Object what you would use in next.config.js - @default {}
   */
78
  conf?: NextConfig
J
JJ Kasper 已提交
79
  dev?: boolean
80
}
81

N
nkzawa 已提交
82
export default class Server {
83 84 85 86
  dir: string
  quiet: boolean
  nextConfig: NextConfig
  distDir: string
87
  pagesDir?: string
88
  publicDir: string
89
  hasStaticDir: boolean
90 91
  serverBuildDir: string
  pagesManifest?: { [name: string]: string }
92 93
  buildId: string
  renderOpts: {
T
Tim Neutkens 已提交
94
    poweredByHeader: boolean
T
Tim Neutkens 已提交
95
    ampBindInitData: boolean
J
Joe Haddad 已提交
96 97 98 99
    staticMarkup: boolean
    buildId: string
    generateEtags: boolean
    runtimeConfig?: { [key: string]: any }
100 101
    assetPrefix?: string
    canonicalBase: string
102
    documentMiddlewareEnabled: boolean
J
Joe Haddad 已提交
103
    hasCssMode: boolean
104
    dev?: boolean
105
  }
106
  private compression?: Middleware
J
JJ Kasper 已提交
107
  private onErrorMiddleware?: ({ err }: { err: Error }) => Promise<void>
108
  router: Router
109
  protected dynamicRoutes?: DynamicRoutes
J
JJ Kasper 已提交
110 111 112
  protected customRoutes?: {
    rewrites: Rewrite[]
    redirects: Redirect[]
113
    headers: Header[]
J
JJ Kasper 已提交
114
  }
115

J
Joe Haddad 已提交
116 117 118 119 120
  public constructor({
    dir = '.',
    staticMarkup = false,
    quiet = false,
    conf = null,
J
JJ Kasper 已提交
121
    dev = false,
J
Joe Haddad 已提交
122
  }: ServerConstructor = {}) {
N
nkzawa 已提交
123
    this.dir = resolve(dir)
N
Naoyuki Kanezawa 已提交
124
    this.quiet = quiet
T
Tim Neutkens 已提交
125
    const phase = this.currentPhase()
126
    this.nextConfig = loadConfig(phase, this.dir, conf)
127
    this.distDir = join(this.dir, this.nextConfig.distDir)
128
    this.publicDir = join(this.dir, CLIENT_PUBLIC_FILES_PATH)
129
    this.hasStaticDir = fs.existsSync(join(this.dir, 'static'))
T
Tim Neutkens 已提交
130

131 132
    // Only serverRuntimeConfig needs the default
    // publicRuntimeConfig gets it's default in client/index.js
J
Joe Haddad 已提交
133 134 135 136 137
    const {
      serverRuntimeConfig = {},
      publicRuntimeConfig,
      assetPrefix,
      generateEtags,
138
      compress,
J
Joe Haddad 已提交
139
    } = this.nextConfig
140

T
Tim Neutkens 已提交
141
    this.buildId = this.readBuildId()
142

143
    this.renderOpts = {
T
Tim Neutkens 已提交
144
      ampBindInitData: this.nextConfig.experimental.ampBindInitData,
T
Tim Neutkens 已提交
145
      poweredByHeader: this.nextConfig.poweredByHeader,
146
      canonicalBase: this.nextConfig.amp.canonicalBase,
147 148
      documentMiddlewareEnabled: this.nextConfig.experimental
        .documentMiddleware,
J
Joe Haddad 已提交
149
      hasCssMode: this.nextConfig.experimental.css,
150
      staticMarkup,
151
      buildId: this.buildId,
152
      generateEtags,
153
    }
N
Naoyuki Kanezawa 已提交
154

155 156
    // Only the `publicRuntimeConfig` key is exposed to the client side
    // It'll be rendered as part of __NEXT_DATA__ on the client side
157
    if (Object.keys(publicRuntimeConfig).length > 0) {
158
      this.renderOpts.runtimeConfig = publicRuntimeConfig
159 160
    }

161
    if (compress && this.nextConfig.target === 'server') {
162 163 164
      this.compression = compression() as Middleware
    }

165
    // Initialize next/config with the environment configuration
166 167 168 169 170 171
    if (this.nextConfig.target === 'server') {
      envConfig.setConfig({
        serverRuntimeConfig,
        publicRuntimeConfig,
      })
    }
172

173 174 175 176 177 178 179 180 181 182
    this.serverBuildDir = join(
      this.distDir,
      this._isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY
    )
    const pagesManifestPath = join(this.serverBuildDir, PAGES_MANIFEST)

    if (!dev) {
      this.pagesManifest = require(pagesManifestPath)
    }

J
JJ Kasper 已提交
183
    this.router = new Router(this.generateRoutes())
184
    this.setAssetPrefix(assetPrefix)
J
JJ Kasper 已提交
185

186 187 188
    // call init-server middleware, this is also handled
    // individually in serverless bundles when deployed
    if (!dev && this.nextConfig.experimental.plugins) {
189 190
      const initServer = require(join(this.serverBuildDir, 'init-server.js'))
        .default
191
      this.onErrorMiddleware = require(join(
192
        this.serverBuildDir,
193 194 195 196 197
        'on-error-server.js'
      )).default
      initServer()
    }

J
JJ Kasper 已提交
198 199 200 201 202 203 204 205 206 207 208 209
    initializeSprCache({
      dev,
      distDir: this.distDir,
      pagesDir: join(
        this.distDir,
        this._isLikeServerless
          ? SERVERLESS_DIRECTORY
          : `${SERVER_DIRECTORY}/static/${this.buildId}`,
        'pages'
      ),
      flushToDisk: this.nextConfig.experimental.sprFlushToDisk,
    })
N
Naoyuki Kanezawa 已提交
210
  }
N
nkzawa 已提交
211

212
  protected currentPhase(): string {
213
    return PHASE_PRODUCTION_SERVER
214 215
  }

216 217 218 219
  private logError(err: Error): void {
    if (this.onErrorMiddleware) {
      this.onErrorMiddleware({ err })
    }
220 221
    if (this.quiet) return
    // tslint:disable-next-line
222
    console.error(err)
223 224
  }

J
Joe Haddad 已提交
225 226 227
  private handleRequest(
    req: IncomingMessage,
    res: ServerResponse,
228
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
229
  ): Promise<void> {
230
    // Parse url if parsedUrl not provided
231
    if (!parsedUrl || typeof parsedUrl !== 'object') {
232 233
      const url: any = req.url
      parsedUrl = parseUrl(url, true)
234
    }
235

236 237 238
    // Parse the querystring ourselves if the user doesn't handle querystring parsing
    if (typeof parsedUrl.query === 'string') {
      parsedUrl.query = parseQs(parsedUrl.query)
N
Naoyuki Kanezawa 已提交
239
    }
240

T
Tim Neutkens 已提交
241 242 243 244 245 246 247 248 249 250
    if (parsedUrl.pathname!.startsWith(this.nextConfig.experimental.basePath)) {
      // If replace ends up replacing the full url it'll be `undefined`, meaning we have to default it to `/`
      parsedUrl.pathname =
        parsedUrl.pathname!.replace(
          this.nextConfig.experimental.basePath,
          ''
        ) || '/'
      req.url = req.url!.replace(this.nextConfig.experimental.basePath, '')
    }

251
    res.statusCode = 200
252
    return this.run(req, res, parsedUrl).catch(err => {
J
Joe Haddad 已提交
253 254 255 256
      this.logError(err)
      res.statusCode = 500
      res.end('Internal Server Error')
    })
257 258
  }

259
  public getRequestHandler() {
260
    return this.handleRequest.bind(this)
N
nkzawa 已提交
261 262
  }

263
  public setAssetPrefix(prefix?: string) {
264
    this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
265 266
  }

267
  // Backwards compatibility
268
  public async prepare(): Promise<void> {}
N
nkzawa 已提交
269

T
Tim Neutkens 已提交
270
  // Backwards compatibility
271
  protected async close(): Promise<void> {}
T
Tim Neutkens 已提交
272

273
  protected setImmutableAssetCacheControl(res: ServerResponse) {
T
Tim Neutkens 已提交
274
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
N
nkzawa 已提交
275 276
  }

J
JJ Kasper 已提交
277 278 279 280
  protected getCustomRoutes() {
    return require(join(this.distDir, ROUTES_MANIFEST))
  }

281 282 283 284 285 286 287
  protected generateRoutes(): {
    routes: Route[]
    fsRoutes: Route[]
    catchAllRoute: Route
    pageChecker: PageChecker
    dynamicRoutes: DynamicRoutes | undefined
  } {
J
JJ Kasper 已提交
288 289
    this.customRoutes = this.getCustomRoutes()

290 291 292
    const publicRoutes = fs.existsSync(this.publicDir)
      ? this.generatePublicRoutes()
      : []
J
JJ Kasper 已提交
293

294
    const staticFilesRoute = this.hasStaticDir
295 296 297 298 299 300 301
      ? [
          {
            // 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*'),
302
            name: 'static catchall',
303 304 305
            fn: async (req, res, params, parsedUrl) => {
              const p = join(this.dir, 'static', ...(params.path || []))
              await this.serveStatic(req, res, p, parsedUrl)
306 307 308
              return {
                finished: true,
              }
309 310 311 312
            },
          } as Route,
        ]
      : []
313

314
    const fsRoutes: Route[] = [
T
Tim Neutkens 已提交
315
      {
316
        match: route('/_next/static/:path*'),
317 318
        type: 'route',
        name: '_next/static catchall',
319
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
320 321 322
          // 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.
323 324

          // make sure to 404 for /_next/static itself
325 326 327 328 329 330
          if (!params.path) {
            await this.render404(req, res, parsedUrl)
            return {
              finished: true,
            }
          }
331

J
Joe Haddad 已提交
332 333 334 335 336
          if (
            params.path[0] === CLIENT_STATIC_FILES_RUNTIME ||
            params.path[0] === 'chunks' ||
            params.path[0] === this.buildId
          ) {
T
Tim Neutkens 已提交
337
            this.setImmutableAssetCacheControl(res)
338
          }
J
Joe Haddad 已提交
339 340 341
          const p = join(
            this.distDir,
            CLIENT_STATIC_FILES_PATH,
342
            ...(params.path || [])
J
Joe Haddad 已提交
343
          )
344
          await this.serveStatic(req, res, p, parsedUrl)
345 346 347
          return {
            finished: true,
          }
348
        },
349
      },
J
JJ Kasper 已提交
350 351
      {
        match: route('/_next/data/:path*'),
352 353
        type: 'route',
        name: '_next/data catchall',
J
JJ Kasper 已提交
354
        fn: async (req, res, params, _parsedUrl) => {
J
JJ Kasper 已提交
355 356 357
          // 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) {
358 359 360 361
            await this.render404(req, res, _parsedUrl)
            return {
              finished: true,
            }
J
JJ Kasper 已提交
362 363 364 365 366 367
          }
          // 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')) {
368 369 370 371
            await this.render404(req, res, _parsedUrl)
            return {
              finished: true,
            }
J
JJ Kasper 已提交
372 373 374 375 376 377 378
          }

          // re-create page's pathname
          const pathname = `/${params.path.join('/')}`
            .replace(/\.json$/, '')
            .replace(/\/index$/, '/')

J
JJ Kasper 已提交
379 380 381 382 383 384 385 386 387
          req.url = pathname
          const parsedUrl = parseUrl(pathname, true)
          await this.render(
            req,
            res,
            pathname,
            { _nextSprData: '1' },
            parsedUrl
          )
388 389 390
          return {
            finished: true,
          }
J
JJ Kasper 已提交
391 392
        },
      },
T
Tim Neutkens 已提交
393
      {
394
        match: route('/_next/:path*'),
395 396
        type: 'route',
        name: '_next catchall',
T
Tim Neutkens 已提交
397
        // This path is needed because `render()` does a check for `/_next` and the calls the routing again
398
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
399
          await this.render404(req, res, parsedUrl)
400 401 402
          return {
            finished: true,
          }
L
Lukáš Huvar 已提交
403 404
        },
      },
405 406
      ...publicRoutes,
      ...staticFilesRoute,
T
Tim Neutkens 已提交
407
    ]
408
    const routes: Route[] = []
409

J
JJ Kasper 已提交
410
    if (this.customRoutes) {
411
      const { redirects, rewrites, headers } = this.customRoutes
J
JJ Kasper 已提交
412 413

      const getCustomRoute = (
414 415
        r: Rewrite | Redirect | Header,
        type: RouteType
J
JJ Kasper 已提交
416 417 418 419 420 421
      ) => ({
        ...r,
        type,
        matcher: getCustomRouteMatcher(r.source),
      })

422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440
      // Headers come very first
      routes.push(
        ...headers.map(r => {
          const route = getCustomRoute(r, 'header')
          return {
            check: true,
            match: route.matcher,
            type: route.type,
            name: `${route.type} ${route.source} header route`,
            fn: async (_req, res, _params, _parsedUrl) => {
              for (const header of (route as Header).headers) {
                res.setHeader(header.key, header.value)
              }
              return { finished: false }
            },
          } as Route
        })
      )

J
JJ Kasper 已提交
441 442 443 444 445 446
      const customRoutes = [
        ...redirects.map(r => getCustomRoute(r, 'redirect')),
        ...rewrites.map(r => getCustomRoute(r, 'rewrite')),
      ]

      routes.push(
447
        ...customRoutes.map(route => {
J
JJ Kasper 已提交
448
          return {
449
            check: true,
450 451
            match: route.matcher,
            type: route.type,
452
            statusCode: (route as Redirect).statusCode,
453 454
            name: `${route.type} ${route.source} route`,
            fn: async (_req, res, params, _parsedUrl) => {
455
              const parsedDestination = parseUrl(route.destination, true)
456
              const destQuery = parsedDestination.query
457 458 459
              let destinationCompiler = compilePathToRegex(
                `${parsedDestination.pathname!}${parsedDestination.hash || ''}`
              )
460 461
              let newUrl

462 463 464 465 466 467 468 469 470 471 472
              Object.keys(destQuery).forEach(key => {
                const val = destQuery[key]
                if (
                  typeof val === 'string' &&
                  val.startsWith(':') &&
                  params[val.substr(1)]
                ) {
                  destQuery[key] = params[val.substr(1)]
                }
              })

473 474 475 476
              try {
                newUrl = destinationCompiler(params)
              } catch (err) {
                if (
J
Joe Haddad 已提交
477 478 479
                  err.message.match(
                    /Expected .*? to not repeat, but got an array/
                  )
480 481 482 483 484 485 486
                ) {
                  throw new Error(
                    `To use a multi-match in the destination you must add \`*\` at the end of the param name to signify it should repeat. https://err.sh/zeit/next.js/invalid-multi-match`
                  )
                }
                throw err
              }
487 488

              if (route.type === 'redirect') {
489 490 491 492 493 494 495
                const parsedNewUrl = parseUrl(newUrl)
                res.setHeader(
                  'Location',
                  formatUrl({
                    ...parsedDestination,
                    pathname: parsedNewUrl.pathname,
                    hash: parsedNewUrl.hash,
496
                    search: undefined,
497 498
                  })
                )
499 500
                res.statusCode =
                  (route as Redirect).statusCode || DEFAULT_REDIRECT_STATUS
501 502 503
                res.end()
                return {
                  finished: true,
J
JJ Kasper 已提交
504
                }
505 506
              } else {
                ;(_req as any)._nextDidRewrite = true
J
JJ Kasper 已提交
507 508
              }

509 510 511
              return {
                finished: false,
                pathname: newUrl,
512
                query: parsedDestination.query,
J
JJ Kasper 已提交
513 514 515 516 517 518 519
              }
            },
          } as Route
        })
      )
    }

520 521
    const catchAllRoute: Route = {
      match: route('/:path*'),
522
      type: 'route',
523
      name: 'Catchall render',
524
      fn: async (req, res, params, parsedUrl) => {
525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548
        const { pathname, query } = parsedUrl
        if (!pathname) {
          throw new Error('pathname is undefined')
        }

        // Used in development to check public directory paths
        if (await this._beforeCatchAllRender(req, res, params, parsedUrl)) {
          return {
            finished: true,
          }
        }

        if (params && params.path && params.path[0] === 'api') {
          const handled = await this.handleApiRequest(
            req as NextApiRequest,
            res as NextApiResponse,
            pathname!
          )
          if (handled) {
            return { finished: true }
          }
        }

        await this.render(req, res, pathname, query, parsedUrl)
549 550 551 552
        return {
          finished: true,
        }
      },
553
    }
554

555
    if (this.nextConfig.useFileSystemPublicRoutes) {
J
Joe Haddad 已提交
556
      this.dynamicRoutes = this.getDynamicRoutes()
J
Joe Haddad 已提交
557

558 559 560
      // 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.
561
      // See more: https://github.com/zeit/next.js/issues/2617
562
      routes.push(catchAllRoute)
563
    }
N
nkzawa 已提交
564

565 566 567 568 569 570 571
    return {
      routes,
      fsRoutes,
      catchAllRoute,
      dynamicRoutes: this.dynamicRoutes,
      pageChecker: this.hasPage.bind(this),
    }
T
Tim Neutkens 已提交
572 573
  }

574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591
  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
  }

592 593 594 595 596 597 598 599 600
  protected async _beforeCatchAllRender(
    _req: IncomingMessage,
    _res: ServerResponse,
    _params: Params,
    _parsedUrl: UrlWithParsedQuery
  ) {
    return false
  }

601 602 603
  // Used to build API page in development
  protected async ensureApiPage(pathname: string) {}

L
Lukáš Huvar 已提交
604 605 606 607 608 609
  /**
   * Resolves `API` request, in development builds on demand
   * @param req http request
   * @param res http response
   * @param pathname path of request
   */
J
Joe Haddad 已提交
610
  private async handleApiRequest(
611 612
    req: IncomingMessage,
    res: ServerResponse,
613
    pathname: string
J
Joe Haddad 已提交
614
  ) {
615
    let page = pathname
L
Lukáš Huvar 已提交
616
    let params: Params | boolean = false
617
    let pageFound = await this.hasPage(page)
J
JJ Kasper 已提交
618

619
    if (!pageFound && this.dynamicRoutes) {
L
Lukáš Huvar 已提交
620 621 622
      for (const dynamicRoute of this.dynamicRoutes) {
        params = dynamicRoute.match(pathname)
        if (params) {
623 624
          page = dynamicRoute.page
          pageFound = true
L
Lukáš Huvar 已提交
625 626 627 628 629
          break
        }
      }
    }

630
    if (!pageFound) {
631
      return false
J
JJ Kasper 已提交
632
    }
633 634 635 636 637 638
    // 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)
J
JJ Kasper 已提交
639

640
    if (!this.renderOpts.dev && this._isLikeServerless) {
641
      if (typeof pageModule.default === 'function') {
642 643
        await pageModule.default(req, res)
        return true
J
JJ Kasper 已提交
644 645 646
      }
    }

647
    await apiResolver(req, res, params, pageModule, this.onErrorMiddleware)
648
    return true
L
Lukáš Huvar 已提交
649 650
  }

651
  protected generatePublicRoutes(): Route[] {
652 653 654
    const routes: Route[] = []
    const publicFiles = recursiveReadDirSync(this.publicDir)

655
    publicFiles.forEach(path => {
656 657
      const unixPath = path.replace(/\\/g, '/')
      // Only include public files that will not replace a page path
658 659
      // this should not occur now that we check this during build
      if (!this.pagesManifest![unixPath]) {
660 661
        routes.push({
          match: route(unixPath),
662 663
          type: 'route',
          name: 'public catchall',
664 665 666
          fn: async (req, res, _params, parsedUrl) => {
            const p = join(this.publicDir, unixPath)
            await this.serveStatic(req, res, p, parsedUrl)
667 668 669
            return {
              finished: true,
            }
670 671 672 673 674 675 676 677
          },
        })
      }
    })

    return routes
  }

678
  protected getDynamicRoutes() {
679 680 681
    const dynamicRoutedPages = Object.keys(this.pagesManifest!).filter(
      isDynamicRoute
    )
682 683 684 685
    return getSortedRoutes(dynamicRoutedPages).map(page => ({
      page,
      match: getRouteMatcher(getRouteRegex(page)),
    }))
J
Joe Haddad 已提交
686 687
  }

688 689 690 691 692 693
  private handleCompression(req: IncomingMessage, res: ServerResponse) {
    if (this.compression) {
      this.compression(req, res, () => {})
    }
  }

694
  protected async run(
J
Joe Haddad 已提交
695 696
    req: IncomingMessage,
    res: ServerResponse,
697
    parsedUrl: UrlWithParsedQuery
J
Joe Haddad 已提交
698
  ) {
699 700
    this.handleCompression(req, res)

701
    try {
702 703
      const matched = await this.router.execute(req, res, parsedUrl)
      if (matched) {
704 705 706 707 708 709 710 711
        return
      }
    } catch (err) {
      if (err.code === 'DECODE_FAILED') {
        res.statusCode = 400
        return this.renderError(null, req, res, '/_error', {})
      }
      throw err
712 713
    }

714
    await this.render404(req, res, parsedUrl)
N
nkzawa 已提交
715 716
  }

717
  protected async sendHTML(
J
Joe Haddad 已提交
718 719
    req: IncomingMessage,
    res: ServerResponse,
720
    html: string
J
Joe Haddad 已提交
721
  ) {
T
Tim Neutkens 已提交
722 723
    const { generateEtags, poweredByHeader } = this.renderOpts
    return sendHTML(req, res, html, { generateEtags, poweredByHeader })
724 725
  }

J
Joe Haddad 已提交
726 727 728 729 730
  public async render(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
731
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
732
  ): Promise<void> {
733
    const url: any = req.url
734 735 736 737 738

    if (
      url.match(/^\/_next\//) ||
      (this.hasStaticDir && url.match(/^\/static\//))
    ) {
739 740 741
      return this.handleRequest(req, res, parsedUrl)
    }

742
    if (isBlockedPage(pathname)) {
743
      return this.render404(req, res, parsedUrl)
744 745
    }

746
    const html = await this.renderToHTML(req, res, pathname, query, {
J
Joe Haddad 已提交
747 748 749 750 751
      dataOnly:
        (this.renderOpts.ampBindInitData && Boolean(query.dataOnly)) ||
        (req.headers &&
          (req.headers.accept || '').indexOf('application/amp.bind+json') !==
            -1),
752
    })
753 754
    // Request was ended by the user
    if (html === null) {
755 756 757
      return
    }

758
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
759
  }
N
nkzawa 已提交
760

J
Joe Haddad 已提交
761
  private async findPageComponents(
J
Joe Haddad 已提交
762
    pathname: string,
763
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
764
  ) {
765
    const serverless = !this.renderOpts.dev && this._isLikeServerless
J
JJ Kasper 已提交
766 767 768
    // try serving a static AMP version first
    if (query.amp) {
      try {
J
Joe Haddad 已提交
769 770 771 772
        return await loadComponents(
          this.distDir,
          this.buildId,
          (pathname === '/' ? '/index' : pathname) + '.amp',
773
          serverless
J
Joe Haddad 已提交
774
        )
J
JJ Kasper 已提交
775 776 777 778
      } catch (err) {
        if (err.code !== 'ENOENT') throw err
      }
    }
J
Joe Haddad 已提交
779 780 781 782
    return await loadComponents(
      this.distDir,
      this.buildId,
      pathname,
783
      serverless
J
Joe Haddad 已提交
784 785 786
    )
  }

J
JJ Kasper 已提交
787 788 789 790 791 792
  private __sendPayload(
    res: ServerResponse,
    payload: any,
    type: string,
    revalidate?: number | false
  ) {
J
JJ Kasper 已提交
793
    // TODO: ETag? Cache-Control headers? Next-specific headers?
J
JJ Kasper 已提交
794
    res.setHeader('Content-Type', type)
J
JJ Kasper 已提交
795
    res.setHeader('Content-Length', Buffer.byteLength(payload))
J
Joe Haddad 已提交
796 797 798 799 800 801 802 803 804 805 806 807
    if (!this.renderOpts.dev) {
      if (revalidate) {
        res.setHeader(
          'Cache-Control',
          `s-maxage=${revalidate}, stale-while-revalidate`
        )
      } else if (revalidate === false) {
        res.setHeader(
          'Cache-Control',
          `s-maxage=31536000, stale-while-revalidate`
        )
      }
J
JJ Kasper 已提交
808
    }
J
JJ Kasper 已提交
809 810 811
    res.end(payload)
  }

J
Joe Haddad 已提交
812 813 814 815 816 817
  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
    result: LoadComponentsReturnType,
818
    opts: any
J
JJ Kasper 已提交
819
  ): Promise<string | null> {
J
JJ Kasper 已提交
820
    // handle static page
J
Joe Haddad 已提交
821 822 823 824
    if (typeof result.Component === 'string') {
      return result.Component
    }

J
JJ Kasper 已提交
825 826
    // check request state
    const isLikeServerless =
J
Joe Haddad 已提交
827
      typeof result.Component === 'object' &&
J
JJ Kasper 已提交
828
      typeof result.Component.renderReqToHTML === 'function'
J
JJ Kasper 已提交
829 830 831 832 833 834
    const isSpr = !!result.unstable_getStaticProps

    // non-spr requests should render like normal
    if (!isSpr) {
      // handle serverless
      if (isLikeServerless) {
835 836 837
        const curUrl = parseUrl(req.url!, true)
        req.url = formatUrl({
          ...curUrl,
838
          search: undefined,
839 840 841 842 843
          query: {
            ...curUrl.query,
            ...query,
          },
        })
J
JJ Kasper 已提交
844 845 846 847 848 849 850 851 852 853 854
        return result.Component.renderReqToHTML(req, res)
      }

      return renderToHTML(req, res, pathname, query, {
        ...result,
        ...opts,
      })
    }

    // Toggle whether or not this is an SPR Data request
    const isSprData = isSpr && query._nextSprData
855 856
    delete query._nextSprData

J
JJ Kasper 已提交
857 858 859 860 861 862 863 864 865 866 867 868 869
    // Compute the SPR cache key
    const sprCacheKey = parseUrl(req.url || '').pathname!

    // Complete the response with cached data if its present
    const cachedData = await getSprCache(sprCacheKey)
    if (cachedData) {
      const data = isSprData
        ? JSON.stringify(cachedData.pageData)
        : cachedData.html

      this.__sendPayload(
        res,
        data,
J
JJ Kasper 已提交
870 871
        isSprData ? 'application/json' : 'text/html; charset=utf-8',
        cachedData.curRevalidate
J
JJ Kasper 已提交
872 873 874 875 876 877
      )

      // Stop the request chain here if the data we sent was up-to-date
      if (!cachedData.isStale) {
        return null
      }
J
JJ Kasper 已提交
878
    }
J
Joe Haddad 已提交
879

J
JJ Kasper 已提交
880 881 882 883 884
    // If we're here, that means data is missing or it's stale.

    // Serverless requests need its URL transformed back into the original
    // request path (to emulate lambda behavior in production)
    if (isLikeServerless && isSprData) {
J
JJ Kasper 已提交
885 886 887
      let { pathname } = parseUrl(req.url || '', true)
      pathname = !pathname || pathname === '/' ? '/index' : pathname
      req.url = `/_next/data/${this.buildId}${pathname}.json`
J
JJ Kasper 已提交
888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919
    }

    const doRender = withCoalescedInvoke(async function(): Promise<{
      html: string | null
      sprData: any
      sprRevalidate: number | false
    }> {
      let sprData: any
      let html: string | null
      let sprRevalidate: number | false

      let renderResult
      // handle serverless
      if (isLikeServerless) {
        renderResult = await result.Component.renderReqToHTML(req, res, true)

        html = renderResult.html
        sprData = renderResult.renderOpts.sprData
        sprRevalidate = renderResult.renderOpts.revalidate
      } else {
        const renderOpts = {
          ...result,
          ...opts,
        }
        renderResult = await renderToHTML(req, res, pathname, query, renderOpts)

        html = renderResult
        sprData = renderOpts.sprData
        sprRevalidate = renderOpts.revalidate
      }

      return { html, sprData, sprRevalidate }
920
    })
J
JJ Kasper 已提交
921 922 923 924 925 926 927 928

    return doRender(sprCacheKey, []).then(
      async ({ isOrigin, value: { html, sprData, sprRevalidate } }) => {
        // Respond to the request if a payload wasn't sent above (from cache)
        if (!isResSent(res)) {
          this.__sendPayload(
            res,
            isSprData ? JSON.stringify(sprData) : html,
J
JJ Kasper 已提交
929 930
            isSprData ? 'application/json' : 'text/html; charset=utf-8',
            sprRevalidate
J
JJ Kasper 已提交
931 932 933 934 935 936 937 938 939 940 941 942 943 944 945
          )
        }

        // Update the SPR cache if the head request
        if (isOrigin) {
          await setSprCache(
            sprCacheKey,
            { html: html!, pageData: sprData },
            sprRevalidate
          )
        }

        return null
      }
    )
946 947
  }

J
Joe Haddad 已提交
948
  public renderToHTML(
J
Joe Haddad 已提交
949 950 951 952
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
J
Joe Haddad 已提交
953 954 955 956 957 958 959
    {
      amphtml,
      dataOnly,
      hasAmp,
    }: {
      amphtml?: boolean
      hasAmp?: boolean
960 961
      dataOnly?: boolean
    } = {}
J
Joe Haddad 已提交
962
  ): Promise<string | null> {
J
Joe Haddad 已提交
963 964
    return this.findPageComponents(pathname, query)
      .then(
965
        result => {
J
Joe Haddad 已提交
966 967 968 969
          return this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
970 971 972
            result.unstable_getStaticProps
              ? { _nextSprData: query._nextSprData }
              : query,
J
Joe Haddad 已提交
973
            result,
974
            { ...this.renderOpts, amphtml, hasAmp, dataOnly }
J
Joe Haddad 已提交
975 976
          )
        },
977
        err => {
J
Joe Haddad 已提交
978 979 980 981 982 983 984 985 986 987 988
          if (err.code !== 'ENOENT' || !this.dynamicRoutes) {
            return Promise.reject(err)
          }

          for (const dynamicRoute of this.dynamicRoutes) {
            const params = dynamicRoute.match(pathname)
            if (!params) {
              continue
            }

            return this.findPageComponents(dynamicRoute.page, query).then(
989 990
              result => {
                return this.renderToHTMLWithComponents(
J
Joe Haddad 已提交
991 992 993
                  req,
                  res,
                  dynamicRoute.page,
J
JJ Kasper 已提交
994 995 996 997 998 999 1000
                  // only add params for SPR enabled pages
                  {
                    ...(result.unstable_getStaticProps
                      ? { _nextSprData: query._nextSprData }
                      : query),
                    ...params,
                  },
J
Joe Haddad 已提交
1001
                  result,
J
JJ Kasper 已提交
1002 1003 1004 1005 1006 1007
                  {
                    ...this.renderOpts,
                    amphtml,
                    hasAmp,
                    dataOnly,
                  }
1008
                )
1009
              }
J
Joe Haddad 已提交
1010 1011 1012 1013
            )
          }

          return Promise.reject(err)
1014
        }
J
Joe Haddad 已提交
1015
      )
1016
      .catch(err => {
J
Joe Haddad 已提交
1017 1018 1019 1020 1021 1022 1023 1024 1025
        if (err && err.code === 'ENOENT') {
          res.statusCode = 404
          return this.renderErrorToHTML(null, req, res, pathname, query)
        } else {
          this.logError(err)
          res.statusCode = 500
          return this.renderErrorToHTML(err, req, res, pathname, query)
        }
      })
N
Naoyuki Kanezawa 已提交
1026 1027
  }

J
Joe Haddad 已提交
1028 1029 1030 1031 1032
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1033
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1034 1035 1036
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
1037
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
1038
    )
N
Naoyuki Kanezawa 已提交
1039
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
1040
    if (html === null) {
1041 1042
      return
    }
1043
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
1044 1045
  }

J
Joe Haddad 已提交
1046 1047 1048 1049 1050
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
1051
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1052
  ) {
J
Joe Haddad 已提交
1053
    const result = await this.findPageComponents('/_error', query)
1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072
    let html
    try {
      html = await this.renderToHTMLWithComponents(
        req,
        res,
        '/_error',
        query,
        result,
        {
          ...this.renderOpts,
          err,
        }
      )
    } catch (err) {
      console.error(err)
      res.statusCode = 500
      html = 'Internal Server Error'
    }
    return html
N
Naoyuki Kanezawa 已提交
1073 1074
  }

J
Joe Haddad 已提交
1075 1076 1077
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
1078
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1079
  ): Promise<void> {
1080 1081
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
1082
    if (!pathname) {
1083 1084
      throw new Error('pathname is undefined')
    }
N
Naoyuki Kanezawa 已提交
1085
    res.statusCode = 404
1086
    return this.renderError(null, req, res, pathname, query)
N
Naoyuki Kanezawa 已提交
1087
  }
N
Naoyuki Kanezawa 已提交
1088

J
Joe Haddad 已提交
1089 1090 1091 1092
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
1093
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1094
  ): Promise<void> {
A
Arunoda Susiripala 已提交
1095
    if (!this.isServeableUrl(path)) {
1096
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
1097 1098
    }

1099 1100 1101 1102 1103 1104
    if (!(req.method === 'GET' || req.method === 'HEAD')) {
      res.statusCode = 405
      res.setHeader('Allow', ['GET', 'HEAD'])
      return this.renderError(null, req, res, path)
    }

N
Naoyuki Kanezawa 已提交
1105
    try {
1106
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
1107
    } catch (err) {
T
Tim Neutkens 已提交
1108
      if (err.code === 'ENOENT' || err.statusCode === 404) {
1109
        this.render404(req, res, parsedUrl)
1110 1111 1112
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
1113 1114 1115 1116 1117 1118
      } else {
        throw err
      }
    }
  }

1119
  private isServeableUrl(path: string): boolean {
A
Arunoda Susiripala 已提交
1120 1121
    const resolved = resolve(path)
    if (
1122
      resolved.indexOf(join(this.distDir) + sep) !== 0 &&
1123 1124
      resolved.indexOf(join(this.dir, 'static') + sep) !== 0 &&
      resolved.indexOf(join(this.dir, 'public') + sep) !== 0
A
Arunoda Susiripala 已提交
1125 1126 1127 1128 1129 1130 1131 1132
    ) {
      // Seems like the user is trying to traverse the filesystem.
      return false
    }

    return true
  }

1133
  protected readBuildId(): string {
1134 1135 1136 1137 1138
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
1139
        throw new Error(
1140
          `Could not find a valid build in the '${this.distDir}' directory! Try building your app with 'next build' before starting the server.`
J
Joe Haddad 已提交
1141
        )
1142 1143 1144
      }

      throw err
1145
    }
1146
  }
1147 1148 1149 1150

  private get _isLikeServerless(): boolean {
    return isTargetLikeServerless(this.nextConfig.target)
  }
1151
}