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
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()
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
  private 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
  private async close(): Promise<void> {}
T
Tim Neutkens 已提交
216

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

221
  private 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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

J
JJ Kasper 已提交
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 637 638 639
    // 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 }
640
    })
J
JJ Kasper 已提交
641 642 643 644 645 646 647 648

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

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

        return null
      }
    )
666 667
  }

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

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

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

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

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

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

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

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

    return true
  }

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

      throw err
865
    }
866
  }
867 868 869 870

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