next-server.ts 25.2 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'
5
import { parse as parseQs, ParsedUrlQuery } from 'querystring'
J
Joe Haddad 已提交
6
import { parse as parseUrl, UrlWithParsedQuery } from 'url'
J
Joe Haddad 已提交
7

J
JJ Kasper 已提交
8
import { withCoalescedInvoke } from '../../lib/coalesced-function'
J
Joe Haddad 已提交
9 10
import {
  BUILD_ID_FILE,
11
  CLIENT_PUBLIC_FILES_PATH,
J
Joe Haddad 已提交
12 13
  CLIENT_STATIC_FILES_PATH,
  CLIENT_STATIC_FILES_RUNTIME,
14
  PAGES_MANIFEST,
J
Joe Haddad 已提交
15 16
  PHASE_PRODUCTION_SERVER,
  SERVER_DIRECTORY,
17
  SERVERLESS_DIRECTORY,
T
Tim Neutkens 已提交
18
} from '../lib/constants'
J
Joe Haddad 已提交
19 20 21 22
import {
  getRouteMatcher,
  getRouteRegex,
  getSortedRoutes,
23
  isDynamicRoute,
J
Joe Haddad 已提交
24
} from '../lib/router/utils'
25
import * as envConfig from '../lib/runtime-config'
J
Joe Haddad 已提交
26
import { isResSent, NextApiRequest, NextApiResponse } from '../lib/utils'
27
import { apiResolver } from './api-utils'
28
import loadConfig, { isTargetLikeServerless } from './config'
J
Joe Haddad 已提交
29
import { recursiveReadDirSync } from './lib/recursive-readdir-sync'
30
import { loadComponents, LoadComponentsReturnType } from './load-components'
J
Joe Haddad 已提交
31
import { renderToHTML } from './render'
J
Joe Haddad 已提交
32
import { getPagePath } from './require'
33
import Router, { Params, route, Route, RouteMatch } from './router'
J
Joe Haddad 已提交
34 35
import { sendHTML } from './send-html'
import { serveStatic } from './serve-static'
J
Joe Haddad 已提交
36
import { getSprCache, initializeSprCache, setSprCache } from './spr-cache'
J
Joe Haddad 已提交
37
import { isBlockedPage, isInternalUrl } from './utils'
38 39 40

type NextConfig = any

41 42 43 44 45 46
type Middleware = (
  req: IncomingMessage,
  res: ServerResponse,
  next: (err?: Error) => void
) => void

T
Tim Neutkens 已提交
47
export type ServerConstructor = {
48 49 50
  /**
   * Where the Next project is located - @default '.'
   */
J
Joe Haddad 已提交
51 52
  dir?: string
  staticMarkup?: boolean
53 54 55
  /**
   * Hide error messages containing server information - @default false
   */
J
Joe Haddad 已提交
56
  quiet?: boolean
57 58 59
  /**
   * Object what you would use in next.config.js - @default {}
   */
60
  conf?: NextConfig
J
JJ Kasper 已提交
61
  dev?: boolean
62
}
63

N
nkzawa 已提交
64
export default class Server {
65 66 67 68
  dir: string
  quiet: boolean
  nextConfig: NextConfig
  distDir: string
69
  pagesDir?: string
70
  publicDir: string
J
JJ Kasper 已提交
71
  pagesManifest: string
72 73
  buildId: string
  renderOpts: {
T
Tim Neutkens 已提交
74
    poweredByHeader: boolean
T
Tim Neutkens 已提交
75
    ampBindInitData: boolean
J
Joe Haddad 已提交
76 77 78 79
    staticMarkup: boolean
    buildId: string
    generateEtags: boolean
    runtimeConfig?: { [key: string]: any }
80 81
    assetPrefix?: string
    canonicalBase: string
82
    documentMiddlewareEnabled: boolean
J
Joe Haddad 已提交
83
    hasCssMode: boolean
84
    dev?: boolean
85
  }
86
  private compression?: Middleware
87
  private onErrorMiddleware?: ({ err }: { err: Error }) => void
88
  router: Router
89
  protected dynamicRoutes?: Array<{ page: string; match: RouteMatch }>
90

J
Joe Haddad 已提交
91 92 93 94 95
  public constructor({
    dir = '.',
    staticMarkup = false,
    quiet = false,
    conf = null,
J
JJ Kasper 已提交
96
    dev = false,
J
Joe Haddad 已提交
97
  }: ServerConstructor = {}) {
N
nkzawa 已提交
98
    this.dir = resolve(dir)
N
Naoyuki Kanezawa 已提交
99
    this.quiet = quiet
T
Tim Neutkens 已提交
100
    const phase = this.currentPhase()
101
    this.nextConfig = loadConfig(phase, this.dir, conf)
102
    this.distDir = join(this.dir, this.nextConfig.distDir)
103
    this.publicDir = join(this.dir, CLIENT_PUBLIC_FILES_PATH)
J
JJ Kasper 已提交
104 105
    this.pagesManifest = join(
      this.distDir,
106 107 108
      this.nextConfig.target === 'server'
        ? SERVER_DIRECTORY
        : SERVERLESS_DIRECTORY,
J
JJ Kasper 已提交
109 110
      PAGES_MANIFEST
    )
T
Tim Neutkens 已提交
111

112 113
    // Only serverRuntimeConfig needs the default
    // publicRuntimeConfig gets it's default in client/index.js
J
Joe Haddad 已提交
114 115 116 117 118
    const {
      serverRuntimeConfig = {},
      publicRuntimeConfig,
      assetPrefix,
      generateEtags,
119
      compress,
J
Joe Haddad 已提交
120
    } = this.nextConfig
121

T
Tim Neutkens 已提交
122
    this.buildId = this.readBuildId()
123

124
    this.renderOpts = {
T
Tim Neutkens 已提交
125
      ampBindInitData: this.nextConfig.experimental.ampBindInitData,
T
Tim Neutkens 已提交
126
      poweredByHeader: this.nextConfig.poweredByHeader,
127
      canonicalBase: this.nextConfig.amp.canonicalBase,
128 129
      documentMiddlewareEnabled: this.nextConfig.experimental
        .documentMiddleware,
J
Joe Haddad 已提交
130
      hasCssMode: this.nextConfig.experimental.css,
131
      staticMarkup,
132
      buildId: this.buildId,
133
      generateEtags,
134
    }
N
Naoyuki Kanezawa 已提交
135

136 137
    // Only the `publicRuntimeConfig` key is exposed to the client side
    // It'll be rendered as part of __NEXT_DATA__ on the client side
138
    if (Object.keys(publicRuntimeConfig).length > 0) {
139
      this.renderOpts.runtimeConfig = publicRuntimeConfig
140 141
    }

142
    if (compress && this.nextConfig.target === 'server') {
143 144 145
      this.compression = compression() as Middleware
    }

146
    // Initialize next/config with the environment configuration
147 148 149 150 151 152
    if (this.nextConfig.target === 'server') {
      envConfig.setConfig({
        serverRuntimeConfig,
        publicRuntimeConfig,
      })
    }
153

154 155
    const routes = this.generateRoutes()
    this.router = new Router(routes)
156
    this.setAssetPrefix(assetPrefix)
J
JJ Kasper 已提交
157

158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
    // call init-server middleware, this is also handled
    // individually in serverless bundles when deployed
    if (!dev && this.nextConfig.experimental.plugins) {
      const serverPath = join(
        this.distDir,
        this._isLikeServerless ? 'serverless' : 'server'
      )
      const initServer = require(join(serverPath, 'init-server.js')).default
      this.onErrorMiddleware = require(join(
        serverPath,
        'on-error-server.js'
      )).default
      initServer()
    }

J
JJ Kasper 已提交
173 174 175 176 177 178 179 180 181 182 183 184
    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 已提交
185
  }
N
nkzawa 已提交
186

187
  protected currentPhase(): string {
188
    return PHASE_PRODUCTION_SERVER
189 190
  }

191 192 193 194
  private logError(err: Error): void {
    if (this.onErrorMiddleware) {
      this.onErrorMiddleware({ err })
    }
195 196
    if (this.quiet) return
    // tslint:disable-next-line
197
    console.error(err)
198 199
  }

J
Joe Haddad 已提交
200 201 202
  private handleRequest(
    req: IncomingMessage,
    res: ServerResponse,
203
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
204
  ): Promise<void> {
205
    // Parse url if parsedUrl not provided
206
    if (!parsedUrl || typeof parsedUrl !== 'object') {
207 208
      const url: any = req.url
      parsedUrl = parseUrl(url, true)
209
    }
210

211 212 213
    // 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 已提交
214
    }
215

216
    res.statusCode = 200
217
    return this.run(req, res, parsedUrl).catch(err => {
J
Joe Haddad 已提交
218 219 220 221
      this.logError(err)
      res.statusCode = 500
      res.end('Internal Server Error')
    })
222 223
  }

224
  public getRequestHandler() {
225
    return this.handleRequest.bind(this)
N
nkzawa 已提交
226 227
  }

228
  public setAssetPrefix(prefix?: string) {
229
    this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
230 231
  }

232
  // Backwards compatibility
233
  public async prepare(): Promise<void> {}
N
nkzawa 已提交
234

T
Tim Neutkens 已提交
235
  // Backwards compatibility
236
  protected async close(): Promise<void> {}
T
Tim Neutkens 已提交
237

238
  protected setImmutableAssetCacheControl(res: ServerResponse) {
T
Tim Neutkens 已提交
239
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
N
nkzawa 已提交
240 241
  }

242
  protected generateRoutes(): Route[] {
243 244 245
    const publicRoutes = fs.existsSync(this.publicDir)
      ? this.generatePublicRoutes()
      : []
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260
    const staticFilesRoute = fs.existsSync(join(this.dir, 'static'))
      ? [
          {
            // 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*'),
            fn: async (req, res, params, parsedUrl) => {
              const p = join(this.dir, 'static', ...(params.path || []))
              await this.serveStatic(req, res, p, parsedUrl)
            },
          } as Route,
        ]
      : []
261

262
    const routes: Route[] = [
T
Tim Neutkens 已提交
263
      {
264
        match: route('/_next/static/:path*'),
265
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
266 267 268
          // 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.
269 270 271 272

          // make sure to 404 for /_next/static itself
          if (!params.path) return this.render404(req, res, parsedUrl)

J
Joe Haddad 已提交
273 274 275 276 277
          if (
            params.path[0] === CLIENT_STATIC_FILES_RUNTIME ||
            params.path[0] === 'chunks' ||
            params.path[0] === this.buildId
          ) {
T
Tim Neutkens 已提交
278
            this.setImmutableAssetCacheControl(res)
279
          }
J
Joe Haddad 已提交
280 281 282
          const p = join(
            this.distDir,
            CLIENT_STATIC_FILES_PATH,
283
            ...(params.path || [])
J
Joe Haddad 已提交
284
          )
285
          await this.serveStatic(req, res, p, parsedUrl)
286
        },
287
      },
J
JJ Kasper 已提交
288 289 290
      {
        match: route('/_next/data/:path*'),
        fn: async (req, res, params, _parsedUrl) => {
J
JJ Kasper 已提交
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
          // 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) {
            return this.render404(req, res, _parsedUrl)
          }
          // 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')) {
            return this.render404(req, res, _parsedUrl)
          }

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

J
JJ Kasper 已提交
309 310 311 312 313 314 315 316 317 318 319
          req.url = pathname
          const parsedUrl = parseUrl(pathname, true)
          await this.render(
            req,
            res,
            pathname,
            { _nextSprData: '1' },
            parsedUrl
          )
        },
      },
T
Tim Neutkens 已提交
320
      {
321
        match: route('/_next/:path*'),
T
Tim Neutkens 已提交
322
        // This path is needed because `render()` does a check for `/_next` and the calls the routing again
323
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
324
          await this.render404(req, res, parsedUrl)
325
        },
T
Tim Neutkens 已提交
326
      },
327
      ...publicRoutes,
328
      ...staticFilesRoute,
L
Lukáš Huvar 已提交
329 330 331 332
      {
        match: route('/api/:path*'),
        fn: async (req, res, params, parsedUrl) => {
          const { pathname } = parsedUrl
L
Lukáš Huvar 已提交
333 334 335 336 337
          await this.handleApiRequest(
            req as NextApiRequest,
            res as NextApiResponse,
            pathname!
          )
L
Lukáš Huvar 已提交
338 339
        },
      },
T
Tim Neutkens 已提交
340
    ]
341

342
    if (this.nextConfig.useFileSystemPublicRoutes) {
J
Joe Haddad 已提交
343
      this.dynamicRoutes = this.getDynamicRoutes()
J
Joe Haddad 已提交
344

345 346 347
      // 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.
348
      // See more: https://github.com/zeit/next.js/issues/2617
T
Tim Neutkens 已提交
349
      routes.push({
350
        match: route('/:path*'),
351
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
352
          const { pathname, query } = parsedUrl
353
          if (!pathname) {
354 355
            throw new Error('pathname is undefined')
          }
356

357
          await this.render(req, res, pathname, query, parsedUrl)
358
        },
T
Tim Neutkens 已提交
359
      })
360
    }
N
nkzawa 已提交
361

T
Tim Neutkens 已提交
362 363 364
    return routes
  }

L
Lukáš Huvar 已提交
365 366 367 368 369 370
  /**
   * Resolves `API` request, in development builds on demand
   * @param req http request
   * @param res http response
   * @param pathname path of request
   */
J
Joe Haddad 已提交
371
  private async handleApiRequest(
L
Lukáš Huvar 已提交
372 373
    req: NextApiRequest,
    res: NextApiResponse,
374
    pathname: string
J
Joe Haddad 已提交
375
  ) {
L
Lukáš Huvar 已提交
376
    let params: Params | boolean = false
J
JJ Kasper 已提交
377 378 379 380 381
    let resolverFunction: any

    try {
      resolverFunction = await this.resolveApiRequest(pathname)
    } catch (err) {}
L
Lukáš Huvar 已提交
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396

    if (
      this.dynamicRoutes &&
      this.dynamicRoutes.length > 0 &&
      !resolverFunction
    ) {
      for (const dynamicRoute of this.dynamicRoutes) {
        params = dynamicRoute.match(pathname)
        if (params) {
          resolverFunction = await this.resolveApiRequest(dynamicRoute.page)
          break
        }
      }
    }

J
JJ Kasper 已提交
397 398 399 400
    if (!resolverFunction) {
      return this.render404(req, res)
    }

401
    if (!this.renderOpts.dev && this._isLikeServerless) {
J
JJ Kasper 已提交
402 403 404 405 406 407
      const mod = require(resolverFunction)
      if (typeof mod.default === 'function') {
        return mod.default(req, res)
      }
    }

408
    await apiResolver(
409 410 411 412 413
      req,
      res,
      params,
      resolverFunction ? require(resolverFunction) : undefined
    )
L
Lukáš Huvar 已提交
414 415 416 417 418 419
  }

  /**
   * Resolves path to resolver function
   * @param pathname path of request
   */
420
  protected async resolveApiRequest(pathname: string): Promise<string | null> {
J
Joe Haddad 已提交
421 422 423
    return getPagePath(
      pathname,
      this.distDir,
424
      this._isLikeServerless,
425
      this.renderOpts.dev
J
Joe Haddad 已提交
426
    )
L
Lukáš Huvar 已提交
427 428
  }

429
  protected generatePublicRoutes(): Route[] {
430 431
    const routes: Route[] = []
    const publicFiles = recursiveReadDirSync(this.publicDir)
432 433
    const serverBuildPath = join(
      this.distDir,
434
      this._isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY
435
    )
436 437
    const pagesManifest = require(join(serverBuildPath, PAGES_MANIFEST))

438
    publicFiles.forEach(path => {
439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454
      const unixPath = path.replace(/\\/g, '/')
      // Only include public files that will not replace a page path
      if (!pagesManifest[unixPath]) {
        routes.push({
          match: route(unixPath),
          fn: async (req, res, _params, parsedUrl) => {
            const p = join(this.publicDir, unixPath)
            await this.serveStatic(req, res, p, parsedUrl)
          },
        })
      }
    })

    return routes
  }

455
  protected getDynamicRoutes() {
J
JJ Kasper 已提交
456 457
    const manifest = require(this.pagesManifest)
    const dynamicRoutedPages = Object.keys(manifest).filter(isDynamicRoute)
458 459 460 461
    return getSortedRoutes(dynamicRoutedPages).map(page => ({
      page,
      match: getRouteMatcher(getRouteRegex(page)),
    }))
J
Joe Haddad 已提交
462 463
  }

464 465 466 467 468 469
  private handleCompression(req: IncomingMessage, res: ServerResponse) {
    if (this.compression) {
      this.compression(req, res, () => {})
    }
  }

470
  protected async run(
J
Joe Haddad 已提交
471 472
    req: IncomingMessage,
    res: ServerResponse,
473
    parsedUrl: UrlWithParsedQuery
J
Joe Haddad 已提交
474
  ) {
475 476
    this.handleCompression(req, res)

477 478 479 480 481 482 483 484 485 486 487 488
    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
489 490
    }

491
    await this.render404(req, res, parsedUrl)
N
nkzawa 已提交
492 493
  }

494
  protected async sendHTML(
J
Joe Haddad 已提交
495 496
    req: IncomingMessage,
    res: ServerResponse,
497
    html: string
J
Joe Haddad 已提交
498
  ) {
T
Tim Neutkens 已提交
499 500
    const { generateEtags, poweredByHeader } = this.renderOpts
    return sendHTML(req, res, html, { generateEtags, poweredByHeader })
501 502
  }

J
Joe Haddad 已提交
503 504 505 506 507
  public async render(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
508
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
509
  ): Promise<void> {
510 511
    const url: any = req.url
    if (isInternalUrl(url)) {
512 513 514
      return this.handleRequest(req, res, parsedUrl)
    }

515
    if (isBlockedPage(pathname)) {
516
      return this.render404(req, res, parsedUrl)
517 518
    }

519
    const html = await this.renderToHTML(req, res, pathname, query, {
J
Joe Haddad 已提交
520 521 522 523 524
      dataOnly:
        (this.renderOpts.ampBindInitData && Boolean(query.dataOnly)) ||
        (req.headers &&
          (req.headers.accept || '').indexOf('application/amp.bind+json') !==
            -1),
525
    })
526 527
    // Request was ended by the user
    if (html === null) {
528 529 530
      return
    }

531
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
532
  }
N
nkzawa 已提交
533

J
Joe Haddad 已提交
534
  private async findPageComponents(
J
Joe Haddad 已提交
535
    pathname: string,
536
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
537
  ) {
538
    const serverless = !this.renderOpts.dev && this._isLikeServerless
J
JJ Kasper 已提交
539 540 541
    // try serving a static AMP version first
    if (query.amp) {
      try {
J
Joe Haddad 已提交
542 543 544 545
        return await loadComponents(
          this.distDir,
          this.buildId,
          (pathname === '/' ? '/index' : pathname) + '.amp',
546
          serverless
J
Joe Haddad 已提交
547
        )
J
JJ Kasper 已提交
548 549 550 551
      } catch (err) {
        if (err.code !== 'ENOENT') throw err
      }
    }
J
Joe Haddad 已提交
552 553 554 555
    return await loadComponents(
      this.distDir,
      this.buildId,
      pathname,
556
      serverless
J
Joe Haddad 已提交
557 558 559
    )
  }

J
JJ Kasper 已提交
560 561 562 563 564 565
  private __sendPayload(
    res: ServerResponse,
    payload: any,
    type: string,
    revalidate?: number | false
  ) {
J
JJ Kasper 已提交
566
    // TODO: ETag? Cache-Control headers? Next-specific headers?
J
JJ Kasper 已提交
567
    res.setHeader('Content-Type', type)
J
JJ Kasper 已提交
568
    res.setHeader('Content-Length', Buffer.byteLength(payload))
J
Joe Haddad 已提交
569 570 571 572 573 574 575 576 577 578 579 580
    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 已提交
581
    }
J
JJ Kasper 已提交
582 583 584
    res.end(payload)
  }

J
Joe Haddad 已提交
585 586 587 588 589 590
  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
    result: LoadComponentsReturnType,
591
    opts: any
J
JJ Kasper 已提交
592
  ): Promise<string | null> {
J
JJ Kasper 已提交
593
    // handle static page
J
Joe Haddad 已提交
594 595 596 597
    if (typeof result.Component === 'string') {
      return result.Component
    }

J
JJ Kasper 已提交
598 599
    // check request state
    const isLikeServerless =
J
Joe Haddad 已提交
600
      typeof result.Component === 'object' &&
J
JJ Kasper 已提交
601
      typeof result.Component.renderReqToHTML === 'function'
J
JJ Kasper 已提交
602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634
    const isSpr = !!result.unstable_getStaticProps

    // non-spr requests should render like normal
    if (!isSpr) {
      // handle serverless
      if (isLikeServerless) {
        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
    if (isSprData) {
      delete query._nextSprData
    }
    // 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 已提交
635 636
        isSprData ? 'application/json' : 'text/html; charset=utf-8',
        cachedData.curRevalidate
J
JJ Kasper 已提交
637 638 639 640 641 642
      )

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

J
JJ Kasper 已提交
645 646 647 648 649
    // 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 已提交
650 651 652
      let { pathname } = parseUrl(req.url || '', true)
      pathname = !pathname || pathname === '/' ? '/index' : pathname
      req.url = `/_next/data/${this.buildId}${pathname}.json`
J
JJ Kasper 已提交
653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684
    }

    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 }
685
    })
J
JJ Kasper 已提交
686 687 688 689 690 691 692 693

    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 已提交
694 695
            isSprData ? 'application/json' : 'text/html; charset=utf-8',
            sprRevalidate
J
JJ Kasper 已提交
696 697 698 699 700 701 702 703 704 705 706 707 708 709 710
          )
        }

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

        return null
      }
    )
711 712
  }

J
Joe Haddad 已提交
713
  public renderToHTML(
J
Joe Haddad 已提交
714 715 716 717
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
J
Joe Haddad 已提交
718 719 720 721 722 723 724
    {
      amphtml,
      dataOnly,
      hasAmp,
    }: {
      amphtml?: boolean
      hasAmp?: boolean
725 726
      dataOnly?: boolean
    } = {}
J
Joe Haddad 已提交
727
  ): Promise<string | null> {
J
Joe Haddad 已提交
728 729
    return this.findPageComponents(pathname, query)
      .then(
730
        result => {
J
Joe Haddad 已提交
731 732 733 734 735 736
          return this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
            query,
            result,
737
            { ...this.renderOpts, amphtml, hasAmp, dataOnly }
J
Joe Haddad 已提交
738 739
          )
        },
740
        err => {
J
Joe Haddad 已提交
741 742 743 744 745 746 747 748 749 750 751
          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(
752 753
              result => {
                return this.renderToHTMLWithComponents(
J
Joe Haddad 已提交
754 755 756
                  req,
                  res,
                  dynamicRoute.page,
J
JJ Kasper 已提交
757 758 759 760 761 762 763
                  // only add params for SPR enabled pages
                  {
                    ...(result.unstable_getStaticProps
                      ? { _nextSprData: query._nextSprData }
                      : query),
                    ...params,
                  },
J
Joe Haddad 已提交
764
                  result,
J
JJ Kasper 已提交
765 766 767 768 769 770
                  {
                    ...this.renderOpts,
                    amphtml,
                    hasAmp,
                    dataOnly,
                  }
771
                )
772
              }
J
Joe Haddad 已提交
773 774 775 776
            )
          }

          return Promise.reject(err)
777
        }
J
Joe Haddad 已提交
778
      )
779
      .catch(err => {
J
Joe Haddad 已提交
780 781 782 783 784 785 786 787 788
        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 已提交
789 790
  }

J
Joe Haddad 已提交
791 792 793 794 795
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
796
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
797 798 799
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
800
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
801
    )
N
Naoyuki Kanezawa 已提交
802
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
803
    if (html === null) {
804 805
      return
    }
806
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
807 808
  }

J
Joe Haddad 已提交
809 810 811 812 813
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
814
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
815
  ) {
J
Joe Haddad 已提交
816
    const result = await this.findPageComponents('/_error', query)
817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835
    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 已提交
836 837
  }

J
Joe Haddad 已提交
838 839 840
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
841
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
842
  ): Promise<void> {
843 844
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
845
    if (!pathname) {
846 847
      throw new Error('pathname is undefined')
    }
N
Naoyuki Kanezawa 已提交
848
    res.statusCode = 404
849
    return this.renderError(null, req, res, pathname, query)
N
Naoyuki Kanezawa 已提交
850
  }
N
Naoyuki Kanezawa 已提交
851

J
Joe Haddad 已提交
852 853 854 855
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
856
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
857
  ): Promise<void> {
A
Arunoda Susiripala 已提交
858
    if (!this.isServeableUrl(path)) {
859
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
860 861
    }

862 863 864 865 866 867
    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 已提交
868
    try {
869
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
870
    } catch (err) {
T
Tim Neutkens 已提交
871
      if (err.code === 'ENOENT' || err.statusCode === 404) {
872
        this.render404(req, res, parsedUrl)
873 874 875
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
876 877 878 879 880 881
      } else {
        throw err
      }
    }
  }

882
  private isServeableUrl(path: string): boolean {
A
Arunoda Susiripala 已提交
883 884
    const resolved = resolve(path)
    if (
885
      resolved.indexOf(join(this.distDir) + sep) !== 0 &&
886 887
      resolved.indexOf(join(this.dir, 'static') + sep) !== 0 &&
      resolved.indexOf(join(this.dir, 'public') + sep) !== 0
A
Arunoda Susiripala 已提交
888 889 890 891 892 893 894 895
    ) {
      // Seems like the user is trying to traverse the filesystem.
      return false
    }

    return true
  }

896
  protected readBuildId(): string {
897 898 899 900 901
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
902 903 904
        throw new Error(
          `Could not find a valid build in the '${
            this.distDir
905
          }' directory! Try building your app with 'next build' before starting the server.`
J
Joe Haddad 已提交
906
        )
907 908 909
      }

      throw err
910
    }
911
  }
912 913 914 915

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