next-server.ts 23.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'
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 { findPagesDir } from '../../lib/find-pages-dir'
J
JJ Kasper 已提交
37
import { initializeSprCache, getSprCache, setSprCache } from './spr-cache'
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
J
JJ Kasper 已提交
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
  router: Router
J
Joe Haddad 已提交
88
  private dynamicRoutes?: Array<{ page: string; match: RouteMatch }>
89

J
Joe Haddad 已提交
90 91 92 93 94
  public constructor({
    dir = '.',
    staticMarkup = false,
    quiet = false,
    conf = null,
J
JJ Kasper 已提交
95
    dev = false,
J
Joe Haddad 已提交
96
  }: ServerConstructor = {}) {
N
nkzawa 已提交
97
    this.dir = resolve(dir)
N
Naoyuki Kanezawa 已提交
98
    this.quiet = quiet
T
Tim Neutkens 已提交
99
    const phase = this.currentPhase()
J
JJ Kasper 已提交
100
    this.pagesDir = findPagesDir(this.dir)
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 147 148
    // Initialize next/config with the environment configuration
    envConfig.setConfig({
      serverRuntimeConfig,
149
      publicRuntimeConfig,
150 151
    })

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

    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 已提交
168
  }
N
nkzawa 已提交
169

170
  private currentPhase(): string {
171
    return PHASE_PRODUCTION_SERVER
172 173
  }

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

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

191 192 193
    // 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 已提交
194
    }
195

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

204
  public getRequestHandler() {
205
    return this.handleRequest.bind(this)
N
nkzawa 已提交
206 207
  }

208
  public setAssetPrefix(prefix?: string) {
209
    this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
210 211
  }

212
  // Backwards compatibility
213
  public async prepare(): Promise<void> {}
N
nkzawa 已提交
214

T
Tim Neutkens 已提交
215
  // Backwards compatibility
216
  private async close(): Promise<void> {}
T
Tim Neutkens 已提交
217

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

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

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

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

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

305
    if (this.nextConfig.useFileSystemPublicRoutes) {
J
Joe Haddad 已提交
306
      this.dynamicRoutes = this.getDynamicRoutes()
J
Joe Haddad 已提交
307

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

320
          await this.render(req, res, pathname, query, parsedUrl)
321
        },
T
Tim Neutkens 已提交
322
      })
323
    }
N
nkzawa 已提交
324

T
Tim Neutkens 已提交
325 326 327
    return routes
  }

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

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

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

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

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

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

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

401
    publicFiles.forEach(path => {
402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417
      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 已提交
418
  private getDynamicRoutes() {
J
JJ Kasper 已提交
419 420
    const manifest = require(this.pagesManifest)
    const dynamicRoutedPages = Object.keys(manifest).filter(isDynamicRoute)
421 422 423 424
    return getSortedRoutes(dynamicRoutedPages).map(page => ({
      page,
      match: getRouteMatcher(getRouteRegex(page)),
    }))
J
Joe Haddad 已提交
425 426
  }

427 428 429 430 431 432
  private handleCompression(req: IncomingMessage, res: ServerResponse) {
    if (this.compression) {
      this.compression(req, res, () => {})
    }
  }

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

440 441 442 443 444 445 446 447 448 449 450 451
    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
452 453
    }

454
    await this.render404(req, res, parsedUrl)
N
nkzawa 已提交
455 456
  }

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

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

478
    if (isBlockedPage(pathname)) {
479
      return this.render404(req, res, parsedUrl)
480 481
    }

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

494
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
495
  }
N
nkzawa 已提交
496

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

J
JJ Kasper 已提交
523 524 525 526 527 528
  private __sendPayload(
    res: ServerResponse,
    payload: any,
    type: string,
    revalidate?: number | false
  ) {
J
JJ Kasper 已提交
529
    // TODO: ETag? Cache-Control headers? Next-specific headers?
J
JJ Kasper 已提交
530
    res.setHeader('Content-Type', type)
J
JJ Kasper 已提交
531
    res.setHeader('Content-Length', Buffer.byteLength(payload))
J
JJ Kasper 已提交
532 533 534 535

    if (revalidate) {
      res.setHeader(
        'Cache-Control',
536
        `s-maxage=${revalidate}, stale-while-revalidate`
J
JJ Kasper 已提交
537 538
      )
    }
J
JJ Kasper 已提交
539 540 541
    res.end(payload)
  }

J
Joe Haddad 已提交
542 543 544 545 546 547
  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
    result: LoadComponentsReturnType,
548
    opts: any
J
JJ Kasper 已提交
549
  ): Promise<string | null> {
J
JJ Kasper 已提交
550
    // handle static page
J
Joe Haddad 已提交
551 552 553 554
    if (typeof result.Component === 'string') {
      return result.Component
    }

J
JJ Kasper 已提交
555 556
    // check request state
    const isLikeServerless =
J
Joe Haddad 已提交
557
      typeof result.Component === 'object' &&
J
JJ Kasper 已提交
558
      typeof result.Component.renderReqToHTML === 'function'
J
JJ Kasper 已提交
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 584 585 586 587 588 589 590 591
    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 已提交
592 593
        isSprData ? 'application/json' : 'text/html; charset=utf-8',
        cachedData.curRevalidate
J
JJ Kasper 已提交
594 595 596 597 598 599
      )

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

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 635 636 637 638 639 640
    // 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 }
641
    })
J
JJ Kasper 已提交
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,
J
JJ Kasper 已提交
650 651
            isSprData ? 'application/json' : 'text/html; charset=utf-8',
            sprRevalidate
J
JJ Kasper 已提交
652 653 654 655 656 657 658 659 660 661 662 663 664 665 666
          )
        }

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

        return null
      }
    )
667 668
  }

J
Joe Haddad 已提交
669
  public renderToHTML(
J
Joe Haddad 已提交
670 671 672 673
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
J
Joe Haddad 已提交
674 675 676 677 678 679 680
    {
      amphtml,
      dataOnly,
      hasAmp,
    }: {
      amphtml?: boolean
      hasAmp?: boolean
681 682
      dataOnly?: boolean
    } = {}
J
Joe Haddad 已提交
683
  ): Promise<string | null> {
J
Joe Haddad 已提交
684 685
    return this.findPageComponents(pathname, query)
      .then(
686
        result => {
J
Joe Haddad 已提交
687 688 689 690 691 692
          return this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
            query,
            result,
693
            { ...this.renderOpts, amphtml, hasAmp, dataOnly }
J
Joe Haddad 已提交
694 695
          )
        },
696
        err => {
J
Joe Haddad 已提交
697 698 699 700 701 702 703 704 705 706 707
          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(
708 709
              result => {
                return this.renderToHTMLWithComponents(
J
Joe Haddad 已提交
710 711 712
                  req,
                  res,
                  dynamicRoute.page,
J
JJ Kasper 已提交
713 714 715 716 717 718 719
                  // only add params for SPR enabled pages
                  {
                    ...(result.unstable_getStaticProps
                      ? { _nextSprData: query._nextSprData }
                      : query),
                    ...params,
                  },
J
Joe Haddad 已提交
720
                  result,
J
JJ Kasper 已提交
721 722 723 724 725 726
                  {
                    ...this.renderOpts,
                    amphtml,
                    hasAmp,
                    dataOnly,
                  }
727
                )
728
              }
J
Joe Haddad 已提交
729 730 731 732
            )
          }

          return Promise.reject(err)
733
        }
J
Joe Haddad 已提交
734
      )
735
      .catch(err => {
J
Joe Haddad 已提交
736 737 738 739 740 741 742 743 744
        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 已提交
745 746
  }

J
Joe Haddad 已提交
747 748 749 750 751
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
752
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
753 754 755
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
756
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
757
    )
N
Naoyuki Kanezawa 已提交
758
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
759
    if (html === null) {
760 761
      return
    }
762
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
763 764
  }

J
Joe Haddad 已提交
765 766 767 768 769
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
770
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
771
  ) {
J
Joe Haddad 已提交
772
    const result = await this.findPageComponents('/_error', query)
773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791
    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 已提交
792 793
  }

J
Joe Haddad 已提交
794 795 796
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
797
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
798
  ): Promise<void> {
799 800
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
801
    if (!pathname) {
802 803
      throw new Error('pathname is undefined')
    }
N
Naoyuki Kanezawa 已提交
804
    res.statusCode = 404
805
    return this.renderError(null, req, res, pathname, query)
N
Naoyuki Kanezawa 已提交
806
  }
N
Naoyuki Kanezawa 已提交
807

J
Joe Haddad 已提交
808 809 810 811
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
812
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
813
  ): Promise<void> {
A
Arunoda Susiripala 已提交
814
    if (!this.isServeableUrl(path)) {
815
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
816 817
    }

818 819 820 821 822 823
    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 已提交
824
    try {
825
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
826
    } catch (err) {
T
Tim Neutkens 已提交
827
      if (err.code === 'ENOENT' || err.statusCode === 404) {
828
        this.render404(req, res, parsedUrl)
829 830 831
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
832 833 834 835 836 837
      } else {
        throw err
      }
    }
  }

838
  private isServeableUrl(path: string): boolean {
A
Arunoda Susiripala 已提交
839 840
    const resolved = resolve(path)
    if (
841
      resolved.indexOf(join(this.distDir) + sep) !== 0 &&
842 843
      resolved.indexOf(join(this.dir, 'static') + sep) !== 0 &&
      resolved.indexOf(join(this.dir, 'public') + sep) !== 0
A
Arunoda Susiripala 已提交
844 845 846 847 848 849 850 851
    ) {
      // Seems like the user is trying to traverse the filesystem.
      return false
    }

    return true
  }

852
  private readBuildId(): string {
853 854 855 856 857
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
858 859 860
        throw new Error(
          `Could not find a valid build in the '${
            this.distDir
861
          }' directory! Try building your app with 'next build' before starting the server.`
J
Joe Haddad 已提交
862
        )
863 864 865
      }

      throw err
866
    }
867
  }
868 869 870 871

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