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

type NextConfig = any

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

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

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

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

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

T
Tim Neutkens 已提交
119
    this.buildId = this.readBuildId()
120

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

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

139
    if (compress && this.nextConfig.target === 'server') {
140 141 142
      this.compression = compression() as Middleware
    }

143 144 145
    // Initialize next/config with the environment configuration
    envConfig.setConfig({
      serverRuntimeConfig,
146
      publicRuntimeConfig,
147 148
    })

149 150
    const routes = this.generateRoutes()
    this.router = new Router(routes)
151
    this.setAssetPrefix(assetPrefix)
J
JJ Kasper 已提交
152 153 154 155 156 157 158 159 160 161 162 163 164

    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 已提交
165
  }
N
nkzawa 已提交
166

167
  private currentPhase(): string {
168
    return PHASE_PRODUCTION_SERVER
169 170
  }

171
  private logError(...args: any): void {
172 173
    if (this.quiet) return
    // tslint:disable-next-line
174 175 176
    console.error(...args)
  }

J
Joe Haddad 已提交
177 178 179
  private handleRequest(
    req: IncomingMessage,
    res: ServerResponse,
180
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
181
  ): Promise<void> {
182
    // Parse url if parsedUrl not provided
183
    if (!parsedUrl || typeof parsedUrl !== 'object') {
184 185
      const url: any = req.url
      parsedUrl = parseUrl(url, true)
186
    }
187

188 189 190
    // 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 已提交
191
    }
192

193
    res.statusCode = 200
194
    return this.run(req, res, parsedUrl).catch(err => {
J
Joe Haddad 已提交
195 196 197 198
      this.logError(err)
      res.statusCode = 500
      res.end('Internal Server Error')
    })
199 200
  }

201
  public getRequestHandler() {
202
    return this.handleRequest.bind(this)
N
nkzawa 已提交
203 204
  }

205
  public setAssetPrefix(prefix?: string) {
206
    this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
207 208
  }

209
  // Backwards compatibility
210
  public async prepare(): Promise<void> {}
N
nkzawa 已提交
211

T
Tim Neutkens 已提交
212
  // Backwards compatibility
213
  private async close(): Promise<void> {}
T
Tim Neutkens 已提交
214

215
  private setImmutableAssetCacheControl(res: ServerResponse) {
T
Tim Neutkens 已提交
216
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
N
nkzawa 已提交
217 218
  }

219
  private generateRoutes(): Route[] {
220
    const routes: Route[] = [
T
Tim Neutkens 已提交
221
      {
222
        match: route('/_next/static/:path*'),
223
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
224 225 226
          // 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.
227 228 229 230

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

J
Joe Haddad 已提交
231 232 233 234 235
          if (
            params.path[0] === CLIENT_STATIC_FILES_RUNTIME ||
            params.path[0] === 'chunks' ||
            params.path[0] === this.buildId
          ) {
T
Tim Neutkens 已提交
236
            this.setImmutableAssetCacheControl(res)
237
          }
J
Joe Haddad 已提交
238 239 240
          const p = join(
            this.distDir,
            CLIENT_STATIC_FILES_PATH,
241
            ...(params.path || [])
J
Joe Haddad 已提交
242
          )
243
          await this.serveStatic(req, res, p, parsedUrl)
244
        },
245
      },
J
JJ Kasper 已提交
246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263
      {
        match: route('/_next/data/:path*'),
        fn: async (req, res, params, _parsedUrl) => {
          // Make sure to 404 for /_next/data/ itself
          if (!params.path) return this.render404(req, res, _parsedUrl)
          // TODO: force `.json` to be present
          const pathname = `/${params.path.join('/')}`.replace(/\.json$/, '')
          req.url = pathname
          const parsedUrl = parseUrl(pathname, true)
          await this.render(
            req,
            res,
            pathname,
            { _nextSprData: '1' },
            parsedUrl
          )
        },
      },
T
Tim Neutkens 已提交
264
      {
265
        match: route('/_next/:path*'),
T
Tim Neutkens 已提交
266
        // This path is needed because `render()` does a check for `/_next` and the calls the routing again
267
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
268
          await this.render404(req, res, parsedUrl)
269
        },
T
Tim Neutkens 已提交
270 271
      },
      {
272 273 274
        // 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.
T
Tim Neutkens 已提交
275
        // See more: https://github.com/zeit/next.js/issues/2617
276
        match: route('/static/:path*'),
277
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
278
          const p = join(this.dir, 'static', ...(params.path || []))
279
          await this.serveStatic(req, res, p, parsedUrl)
280 281
        },
      },
L
Lukáš Huvar 已提交
282 283 284 285
      {
        match: route('/api/:path*'),
        fn: async (req, res, params, parsedUrl) => {
          const { pathname } = parsedUrl
L
Lukáš Huvar 已提交
286 287 288 289 290
          await this.handleApiRequest(
            req as NextApiRequest,
            res as NextApiResponse,
            pathname!
          )
L
Lukáš Huvar 已提交
291 292
        },
      },
T
Tim Neutkens 已提交
293
    ]
294

J
Joe Haddad 已提交
295 296 297 298
    if (
      this.nextConfig.experimental.publicDirectory &&
      fs.existsSync(this.publicDir)
    ) {
299 300 301
      routes.push(...this.generatePublicRoutes())
    }

302
    if (this.nextConfig.useFileSystemPublicRoutes) {
J
Joe Haddad 已提交
303
      this.dynamicRoutes = this.getDynamicRoutes()
J
Joe Haddad 已提交
304

305 306 307
      // 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.
308
      // See more: https://github.com/zeit/next.js/issues/2617
T
Tim Neutkens 已提交
309
      routes.push({
310
        match: route('/:path*'),
311
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
312
          const { pathname, query } = parsedUrl
313
          if (!pathname) {
314 315
            throw new Error('pathname is undefined')
          }
316

317
          await this.render(req, res, pathname, query, parsedUrl)
318
        },
T
Tim Neutkens 已提交
319
      })
320
    }
N
nkzawa 已提交
321

T
Tim Neutkens 已提交
322 323 324
    return routes
  }

L
Lukáš Huvar 已提交
325 326 327 328 329 330
  /**
   * Resolves `API` request, in development builds on demand
   * @param req http request
   * @param res http response
   * @param pathname path of request
   */
J
Joe Haddad 已提交
331
  private async handleApiRequest(
L
Lukáš Huvar 已提交
332 333
    req: NextApiRequest,
    res: NextApiResponse,
334
    pathname: string
J
Joe Haddad 已提交
335
  ) {
L
Lukáš Huvar 已提交
336
    let params: Params | boolean = false
J
JJ Kasper 已提交
337 338 339 340 341
    let resolverFunction: any

    try {
      resolverFunction = await this.resolveApiRequest(pathname)
    } catch (err) {}
L
Lukáš Huvar 已提交
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356

    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 已提交
357 358 359 360
    if (!resolverFunction) {
      return this.render404(req, res)
    }

361
    if (!this.renderOpts.dev && this._isLikeServerless) {
J
JJ Kasper 已提交
362 363 364 365 366 367
      const mod = require(resolverFunction)
      if (typeof mod.default === 'function') {
        return mod.default(req, res)
      }
    }

368
    await apiResolver(
369 370 371 372 373
      req,
      res,
      params,
      resolverFunction ? require(resolverFunction) : undefined
    )
L
Lukáš Huvar 已提交
374 375 376 377 378 379 380
  }

  /**
   * Resolves path to resolver function
   * @param pathname path of request
   */
  private resolveApiRequest(pathname: string) {
J
Joe Haddad 已提交
381 382 383
    return getPagePath(
      pathname,
      this.distDir,
384
      this._isLikeServerless,
385
      this.renderOpts.dev
J
Joe Haddad 已提交
386
    )
L
Lukáš Huvar 已提交
387 388
  }

389 390 391
  private generatePublicRoutes(): Route[] {
    const routes: Route[] = []
    const publicFiles = recursiveReadDirSync(this.publicDir)
392 393
    const serverBuildPath = join(
      this.distDir,
394
      this._isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY
395
    )
396 397
    const pagesManifest = require(join(serverBuildPath, PAGES_MANIFEST))

398
    publicFiles.forEach(path => {
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414
      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
  }

J
Joe Haddad 已提交
415
  private getDynamicRoutes() {
J
JJ Kasper 已提交
416 417
    const manifest = require(this.pagesManifest)
    const dynamicRoutedPages = Object.keys(manifest).filter(isDynamicRoute)
418 419 420 421
    return getSortedRoutes(dynamicRoutedPages).map(page => ({
      page,
      match: getRouteMatcher(getRouteRegex(page)),
    }))
J
Joe Haddad 已提交
422 423
  }

424 425 426 427 428 429
  private handleCompression(req: IncomingMessage, res: ServerResponse) {
    if (this.compression) {
      this.compression(req, res, () => {})
    }
  }

J
Joe Haddad 已提交
430 431 432
  private async run(
    req: IncomingMessage,
    res: ServerResponse,
433
    parsedUrl: UrlWithParsedQuery
J
Joe Haddad 已提交
434
  ) {
435 436
    this.handleCompression(req, res)

437 438 439 440 441 442 443 444 445 446 447 448
    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
449 450
    }

451
    await this.render404(req, res, parsedUrl)
N
nkzawa 已提交
452 453
  }

J
Joe Haddad 已提交
454 455 456
  private async sendHTML(
    req: IncomingMessage,
    res: ServerResponse,
457
    html: string
J
Joe Haddad 已提交
458
  ) {
T
Tim Neutkens 已提交
459 460
    const { generateEtags, poweredByHeader } = this.renderOpts
    return sendHTML(req, res, html, { generateEtags, poweredByHeader })
461 462
  }

J
Joe Haddad 已提交
463 464 465 466 467
  public async render(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
468
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
469
  ): Promise<void> {
470 471
    const url: any = req.url
    if (isInternalUrl(url)) {
472 473 474
      return this.handleRequest(req, res, parsedUrl)
    }

475
    if (isBlockedPage(pathname)) {
476
      return this.render404(req, res, parsedUrl)
477 478
    }

479
    const html = await this.renderToHTML(req, res, pathname, query, {
J
Joe Haddad 已提交
480 481 482 483 484
      dataOnly:
        (this.renderOpts.ampBindInitData && Boolean(query.dataOnly)) ||
        (req.headers &&
          (req.headers.accept || '').indexOf('application/amp.bind+json') !==
            -1),
485
    })
486 487
    // Request was ended by the user
    if (html === null) {
488 489 490
      return
    }

491
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
492
  }
N
nkzawa 已提交
493

J
Joe Haddad 已提交
494
  private async findPageComponents(
J
Joe Haddad 已提交
495
    pathname: string,
496
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
497
  ) {
498
    const serverless = !this.renderOpts.dev && this._isLikeServerless
J
JJ Kasper 已提交
499 500 501
    // try serving a static AMP version first
    if (query.amp) {
      try {
J
Joe Haddad 已提交
502 503 504 505
        return await loadComponents(
          this.distDir,
          this.buildId,
          (pathname === '/' ? '/index' : pathname) + '.amp',
506
          serverless
J
Joe Haddad 已提交
507
        )
J
JJ Kasper 已提交
508 509 510 511
      } catch (err) {
        if (err.code !== 'ENOENT') throw err
      }
    }
J
Joe Haddad 已提交
512 513 514 515
    return await loadComponents(
      this.distDir,
      this.buildId,
      pathname,
516
      serverless
J
Joe Haddad 已提交
517 518 519
    )
  }

J
JJ Kasper 已提交
520 521 522 523 524 525 526
  private __sendPayload(res: ServerResponse, payload: any, header: string) {
    // TODO: ETag? Cache-Control headers? Next-specific headers?
    res.setHeader('Content-Type', header)
    res.setHeader('Content-Length', Buffer.byteLength(payload))
    res.end(payload)
  }

J
Joe Haddad 已提交
527 528 529 530 531 532
  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
    result: LoadComponentsReturnType,
533
    opts: any
J
JJ Kasper 已提交
534
  ): Promise<string | null> {
J
JJ Kasper 已提交
535
    // handle static page
J
Joe Haddad 已提交
536 537 538 539
    if (typeof result.Component === 'string') {
      return result.Component
    }

J
JJ Kasper 已提交
540 541
    // check request state
    const isLikeServerless =
J
Joe Haddad 已提交
542
      typeof result.Component === 'object' &&
J
JJ Kasper 已提交
543
      typeof result.Component.renderReqToHTML === 'function'
J
JJ Kasper 已提交
544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583
    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,
        isSprData ? 'application/json' : 'text/html; charset=utf-8'
      )

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

J
JJ Kasper 已提交
586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624
    // 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) {
      const curUrl = parseUrl(req.url || '', true)
      req.url = `/_next/data${curUrl.pathname}.json`
    }

    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 }
625
    })
J
JJ Kasper 已提交
626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649

    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,
            isSprData ? 'application/json' : 'text/html; charset=utf-8'
          )
        }

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

        return null
      }
    )
650 651
  }

J
Joe Haddad 已提交
652
  public renderToHTML(
J
Joe Haddad 已提交
653 654 655 656
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
J
Joe Haddad 已提交
657 658 659 660 661 662 663
    {
      amphtml,
      dataOnly,
      hasAmp,
    }: {
      amphtml?: boolean
      hasAmp?: boolean
664 665
      dataOnly?: boolean
    } = {}
J
Joe Haddad 已提交
666
  ): Promise<string | null> {
J
Joe Haddad 已提交
667 668
    return this.findPageComponents(pathname, query)
      .then(
669
        result => {
J
Joe Haddad 已提交
670 671 672 673 674 675
          return this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
            query,
            result,
676
            { ...this.renderOpts, amphtml, hasAmp, dataOnly }
J
Joe Haddad 已提交
677 678
          )
        },
679
        err => {
J
Joe Haddad 已提交
680 681 682 683 684 685 686 687 688 689 690
          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(
691 692
              result => {
                return this.renderToHTMLWithComponents(
J
Joe Haddad 已提交
693 694 695
                  req,
                  res,
                  dynamicRoute.page,
J
JJ Kasper 已提交
696 697 698 699 700 701 702
                  // only add params for SPR enabled pages
                  {
                    ...(result.unstable_getStaticProps
                      ? { _nextSprData: query._nextSprData }
                      : query),
                    ...params,
                  },
J
Joe Haddad 已提交
703
                  result,
J
JJ Kasper 已提交
704 705 706 707 708 709
                  {
                    ...this.renderOpts,
                    amphtml,
                    hasAmp,
                    dataOnly,
                  }
710
                )
711
              }
J
Joe Haddad 已提交
712 713 714 715
            )
          }

          return Promise.reject(err)
716
        }
J
Joe Haddad 已提交
717
      )
718
      .catch(err => {
J
Joe Haddad 已提交
719 720 721 722 723 724 725 726 727
        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 已提交
728 729
  }

J
Joe Haddad 已提交
730 731 732 733 734
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
735
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
736 737 738
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
739
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
740
    )
N
Naoyuki Kanezawa 已提交
741
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
742
    if (html === null) {
743 744
      return
    }
745
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
746 747
  }

J
Joe Haddad 已提交
748 749 750 751 752
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
753
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
754
  ) {
J
Joe Haddad 已提交
755
    const result = await this.findPageComponents('/_error', query)
756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774
    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 已提交
775 776
  }

J
Joe Haddad 已提交
777 778 779
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
780
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
781
  ): Promise<void> {
782 783
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
784
    if (!pathname) {
785 786
      throw new Error('pathname is undefined')
    }
N
Naoyuki Kanezawa 已提交
787
    res.statusCode = 404
788
    return this.renderError(null, req, res, pathname, query)
N
Naoyuki Kanezawa 已提交
789
  }
N
Naoyuki Kanezawa 已提交
790

J
Joe Haddad 已提交
791 792 793 794
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
795
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
796
  ): Promise<void> {
A
Arunoda Susiripala 已提交
797
    if (!this.isServeableUrl(path)) {
798
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
799 800
    }

801 802 803 804 805 806
    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 已提交
807
    try {
808
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
809
    } catch (err) {
T
Tim Neutkens 已提交
810
      if (err.code === 'ENOENT' || err.statusCode === 404) {
811
        this.render404(req, res, parsedUrl)
812 813 814
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
815 816 817 818 819 820
      } else {
        throw err
      }
    }
  }

821
  private isServeableUrl(path: string): boolean {
A
Arunoda Susiripala 已提交
822 823
    const resolved = resolve(path)
    if (
824
      resolved.indexOf(join(this.distDir) + sep) !== 0 &&
825 826
      resolved.indexOf(join(this.dir, 'static') + sep) !== 0 &&
      resolved.indexOf(join(this.dir, 'public') + sep) !== 0
A
Arunoda Susiripala 已提交
827 828 829 830 831 832 833 834
    ) {
      // Seems like the user is trying to traverse the filesystem.
      return false
    }

    return true
  }

835
  private readBuildId(): string {
836 837 838 839 840
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
841 842 843
        throw new Error(
          `Could not find a valid build in the '${
            this.distDir
844
          }' directory! Try building your app with 'next build' before starting the server.`
J
Joe Haddad 已提交
845
        )
846 847 848
      }

      throw err
849
    }
850
  }
851 852 853 854

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