next-server.ts 23.6 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
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
88
  protected 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()
100
    this.nextConfig = loadConfig(phase, this.dir, conf)
101
    this.distDir = join(this.dir, this.nextConfig.distDir)
102
    this.publicDir = join(this.dir, CLIENT_PUBLIC_FILES_PATH)
J
JJ Kasper 已提交
103 104
    this.pagesManifest = join(
      this.distDir,
105 106 107
      this.nextConfig.target === 'server'
        ? SERVER_DIRECTORY
        : SERVERLESS_DIRECTORY,
J
JJ Kasper 已提交
108 109
      PAGES_MANIFEST
    )
T
Tim Neutkens 已提交
110

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

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

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

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

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

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

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

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

169
  protected currentPhase(): string {
170
    return PHASE_PRODUCTION_SERVER
171 172
  }

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

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

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

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

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

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

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

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

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

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

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

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

297
    if (fs.existsSync(this.publicDir)) {
298 299 300
      routes.push(...this.generatePublicRoutes())
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    if (revalidate) {
      res.setHeader(
        'Cache-Control',
532
        `s-maxage=${revalidate}, stale-while-revalidate`
J
JJ Kasper 已提交
533 534
      )
    }
J
JJ Kasper 已提交
535 536 537
    res.end(payload)
  }

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

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

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

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

    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 已提交
646 647
            isSprData ? 'application/json' : 'text/html; charset=utf-8',
            sprRevalidate
J
JJ Kasper 已提交
648 649 650 651 652 653 654 655 656 657 658 659 660 661 662
          )
        }

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

        return null
      }
    )
663 664
  }

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

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

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

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

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

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

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

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

    return true
  }

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

      throw err
862
    }
863
  }
864 865 866 867

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