next-server.ts 24.4 KB
Newer Older
1
import compression from 'compression'
J
Joe Haddad 已提交
2
import fs from 'fs'
J
Joe Haddad 已提交
3
import { IncomingMessage, ServerResponse } from 'http'
J
Joe Haddad 已提交
4
import { join, resolve, sep } from 'path'
5
import { parse as parseQs, ParsedUrlQuery } from 'querystring'
J
Joe Haddad 已提交
6
import { parse as parseUrl, UrlWithParsedQuery } from 'url'
J
Joe Haddad 已提交
7

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

type NextConfig = any

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

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

N
nkzawa 已提交
64
export default class Server {
65 66 67 68
  dir: string
  quiet: boolean
  nextConfig: NextConfig
  distDir: string
69
  pagesDir?: string
70
  publicDir: string
J
JJ Kasper 已提交
71
  pagesManifest: string
72 73
  buildId: string
  renderOpts: {
T
Tim Neutkens 已提交
74
    poweredByHeader: boolean
T
Tim Neutkens 已提交
75
    ampBindInitData: boolean
J
Joe Haddad 已提交
76 77 78 79
    staticMarkup: boolean
    buildId: string
    generateEtags: boolean
    runtimeConfig?: { [key: string]: any }
80 81
    assetPrefix?: string
    canonicalBase: string
82
    documentMiddlewareEnabled: boolean
J
Joe Haddad 已提交
83
    hasCssMode: boolean
84
    dev?: boolean
85
  }
86
  private compression?: Middleware
87
  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
    // Initialize next/config with the environment configuration
146 147 148 149 150 151
    if (this.nextConfig.target === 'server') {
      envConfig.setConfig({
        serverRuntimeConfig,
        publicRuntimeConfig,
      })
    }
152

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

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

171
  protected currentPhase(): string {
172
    return PHASE_PRODUCTION_SERVER
173 174
  }

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

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

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

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

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

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

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

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

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

223
  protected generateRoutes(): Route[] {
224 225 226 227
    const publicRoutes = fs.existsSync(this.publicDir)
      ? this.generatePublicRoutes()
      : []

228
    const routes: Route[] = [
T
Tim Neutkens 已提交
229
      {
230
        match: route('/_next/static/:path*'),
231
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
232 233 234
          // 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.
235 236 237 238

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

J
Joe Haddad 已提交
239 240 241 242 243
          if (
            params.path[0] === CLIENT_STATIC_FILES_RUNTIME ||
            params.path[0] === 'chunks' ||
            params.path[0] === this.buildId
          ) {
T
Tim Neutkens 已提交
244
            this.setImmutableAssetCacheControl(res)
245
          }
J
Joe Haddad 已提交
246 247 248
          const p = join(
            this.distDir,
            CLIENT_STATIC_FILES_PATH,
249
            ...(params.path || [])
J
Joe Haddad 已提交
250
          )
251
          await this.serveStatic(req, res, p, parsedUrl)
252
        },
253
      },
J
JJ Kasper 已提交
254 255 256
      {
        match: route('/_next/data/:path*'),
        fn: async (req, res, params, _parsedUrl) => {
J
JJ Kasper 已提交
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
          // Make sure to 404 for /_next/data/ itself and
          // we also want to 404 if the buildId isn't correct
          if (!params.path || params.path[0] !== this.buildId) {
            return this.render404(req, res, _parsedUrl)
          }
          // remove buildId from URL
          params.path.shift()

          // show 404 if it doesn't end with .json
          if (!params.path[params.path.length - 1].endsWith('.json')) {
            return this.render404(req, res, _parsedUrl)
          }

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

J
JJ Kasper 已提交
275 276 277 278 279 280 281 282 283 284 285
          req.url = pathname
          const parsedUrl = parseUrl(pathname, true)
          await this.render(
            req,
            res,
            pathname,
            { _nextSprData: '1' },
            parsedUrl
          )
        },
      },
T
Tim Neutkens 已提交
286
      {
287
        match: route('/_next/:path*'),
T
Tim Neutkens 已提交
288
        // This path is needed because `render()` does a check for `/_next` and the calls the routing again
289
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
290
          await this.render404(req, res, parsedUrl)
291
        },
T
Tim Neutkens 已提交
292
      },
293
      ...publicRoutes,
T
Tim Neutkens 已提交
294
      {
295 296 297
        // 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 已提交
298
        // See more: https://github.com/zeit/next.js/issues/2617
299
        match: route('/static/:path*'),
300
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
301
          const p = join(this.dir, 'static', ...(params.path || []))
302
          await this.serveStatic(req, res, p, parsedUrl)
303 304
        },
      },
L
Lukáš Huvar 已提交
305 306 307 308
      {
        match: route('/api/:path*'),
        fn: async (req, res, params, parsedUrl) => {
          const { pathname } = parsedUrl
L
Lukáš Huvar 已提交
309 310 311 312 313
          await this.handleApiRequest(
            req as NextApiRequest,
            res as NextApiResponse,
            pathname!
          )
L
Lukáš Huvar 已提交
314 315
        },
      },
T
Tim Neutkens 已提交
316
    ]
317

318
    if (this.nextConfig.useFileSystemPublicRoutes) {
J
Joe Haddad 已提交
319
      this.dynamicRoutes = this.getDynamicRoutes()
J
Joe Haddad 已提交
320

321 322 323
      // 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.
324
      // See more: https://github.com/zeit/next.js/issues/2617
T
Tim Neutkens 已提交
325
      routes.push({
326
        match: route('/:path*'),
327
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
328
          const { pathname, query } = parsedUrl
329
          if (!pathname) {
330 331
            throw new Error('pathname is undefined')
          }
332

333
          await this.render(req, res, pathname, query, parsedUrl)
334
        },
T
Tim Neutkens 已提交
335
      })
336
    }
N
nkzawa 已提交
337

T
Tim Neutkens 已提交
338 339 340
    return routes
  }

L
Lukáš Huvar 已提交
341 342 343 344 345 346
  /**
   * Resolves `API` request, in development builds on demand
   * @param req http request
   * @param res http response
   * @param pathname path of request
   */
J
Joe Haddad 已提交
347
  private async handleApiRequest(
L
Lukáš Huvar 已提交
348 349
    req: NextApiRequest,
    res: NextApiResponse,
350
    pathname: string
J
Joe Haddad 已提交
351
  ) {
L
Lukáš Huvar 已提交
352
    let params: Params | boolean = false
J
JJ Kasper 已提交
353 354 355 356 357
    let resolverFunction: any

    try {
      resolverFunction = await this.resolveApiRequest(pathname)
    } catch (err) {}
L
Lukáš Huvar 已提交
358 359 360 361 362 363 364 365 366 367 368 369 370 371 372

    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 已提交
373 374 375 376
    if (!resolverFunction) {
      return this.render404(req, res)
    }

377
    if (!this.renderOpts.dev && this._isLikeServerless) {
J
JJ Kasper 已提交
378 379 380 381 382 383
      const mod = require(resolverFunction)
      if (typeof mod.default === 'function') {
        return mod.default(req, res)
      }
    }

384
    await apiResolver(
385 386 387 388 389
      req,
      res,
      params,
      resolverFunction ? require(resolverFunction) : undefined
    )
L
Lukáš Huvar 已提交
390 391 392 393 394 395
  }

  /**
   * Resolves path to resolver function
   * @param pathname path of request
   */
396
  protected async resolveApiRequest(pathname: string): Promise<string | null> {
J
Joe Haddad 已提交
397 398 399
    return getPagePath(
      pathname,
      this.distDir,
400
      this._isLikeServerless,
401
      this.renderOpts.dev
J
Joe Haddad 已提交
402
    )
L
Lukáš Huvar 已提交
403 404
  }

405
  protected generatePublicRoutes(): Route[] {
406 407
    const routes: Route[] = []
    const publicFiles = recursiveReadDirSync(this.publicDir)
408 409
    const serverBuildPath = join(
      this.distDir,
410
      this._isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY
411
    )
412 413
    const pagesManifest = require(join(serverBuildPath, PAGES_MANIFEST))

414
    publicFiles.forEach(path => {
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430
      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
  }

431
  protected getDynamicRoutes() {
J
JJ Kasper 已提交
432 433
    const manifest = require(this.pagesManifest)
    const dynamicRoutedPages = Object.keys(manifest).filter(isDynamicRoute)
434 435 436 437
    return getSortedRoutes(dynamicRoutedPages).map(page => ({
      page,
      match: getRouteMatcher(getRouteRegex(page)),
    }))
J
Joe Haddad 已提交
438 439
  }

440 441 442 443 444 445
  private handleCompression(req: IncomingMessage, res: ServerResponse) {
    if (this.compression) {
      this.compression(req, res, () => {})
    }
  }

446
  protected async run(
J
Joe Haddad 已提交
447 448
    req: IncomingMessage,
    res: ServerResponse,
449
    parsedUrl: UrlWithParsedQuery
J
Joe Haddad 已提交
450
  ) {
451 452
    this.handleCompression(req, res)

453 454 455 456 457 458 459 460 461 462 463 464
    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
465 466
    }

467
    await this.render404(req, res, parsedUrl)
N
nkzawa 已提交
468 469
  }

470
  protected async sendHTML(
J
Joe Haddad 已提交
471 472
    req: IncomingMessage,
    res: ServerResponse,
473
    html: string
J
Joe Haddad 已提交
474
  ) {
T
Tim Neutkens 已提交
475 476
    const { generateEtags, poweredByHeader } = this.renderOpts
    return sendHTML(req, res, html, { generateEtags, poweredByHeader })
477 478
  }

J
Joe Haddad 已提交
479 480 481 482 483
  public async render(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
484
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
485
  ): Promise<void> {
486 487
    const url: any = req.url
    if (isInternalUrl(url)) {
488 489 490
      return this.handleRequest(req, res, parsedUrl)
    }

491
    if (isBlockedPage(pathname)) {
492
      return this.render404(req, res, parsedUrl)
493 494
    }

495
    const html = await this.renderToHTML(req, res, pathname, query, {
J
Joe Haddad 已提交
496 497 498 499 500
      dataOnly:
        (this.renderOpts.ampBindInitData && Boolean(query.dataOnly)) ||
        (req.headers &&
          (req.headers.accept || '').indexOf('application/amp.bind+json') !==
            -1),
501
    })
502 503
    // Request was ended by the user
    if (html === null) {
504 505 506
      return
    }

507
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
508
  }
N
nkzawa 已提交
509

J
Joe Haddad 已提交
510
  private async findPageComponents(
J
Joe Haddad 已提交
511
    pathname: string,
512
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
513
  ) {
514
    const serverless = !this.renderOpts.dev && this._isLikeServerless
J
JJ Kasper 已提交
515 516 517
    // try serving a static AMP version first
    if (query.amp) {
      try {
J
Joe Haddad 已提交
518 519 520 521
        return await loadComponents(
          this.distDir,
          this.buildId,
          (pathname === '/' ? '/index' : pathname) + '.amp',
522
          serverless
J
Joe Haddad 已提交
523
        )
J
JJ Kasper 已提交
524 525 526 527
      } catch (err) {
        if (err.code !== 'ENOENT') throw err
      }
    }
J
Joe Haddad 已提交
528 529 530 531
    return await loadComponents(
      this.distDir,
      this.buildId,
      pathname,
532
      serverless
J
Joe Haddad 已提交
533 534 535
    )
  }

J
JJ Kasper 已提交
536 537 538 539 540 541
  private __sendPayload(
    res: ServerResponse,
    payload: any,
    type: string,
    revalidate?: number | false
  ) {
J
JJ Kasper 已提交
542
    // TODO: ETag? Cache-Control headers? Next-specific headers?
J
JJ Kasper 已提交
543
    res.setHeader('Content-Type', type)
J
JJ Kasper 已提交
544
    res.setHeader('Content-Length', Buffer.byteLength(payload))
J
Joe Haddad 已提交
545 546 547 548 549 550 551 552 553 554 555 556
    if (!this.renderOpts.dev) {
      if (revalidate) {
        res.setHeader(
          'Cache-Control',
          `s-maxage=${revalidate}, stale-while-revalidate`
        )
      } else if (revalidate === false) {
        res.setHeader(
          'Cache-Control',
          `s-maxage=31536000, stale-while-revalidate`
        )
      }
J
JJ Kasper 已提交
557
    }
J
JJ Kasper 已提交
558 559 560
    res.end(payload)
  }

J
Joe Haddad 已提交
561 562 563 564 565 566
  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
    result: LoadComponentsReturnType,
567
    opts: any
J
JJ Kasper 已提交
568
  ): Promise<string | null> {
J
JJ Kasper 已提交
569
    // handle static page
J
Joe Haddad 已提交
570 571 572 573
    if (typeof result.Component === 'string') {
      return result.Component
    }

J
JJ Kasper 已提交
574 575
    // check request state
    const isLikeServerless =
J
Joe Haddad 已提交
576
      typeof result.Component === 'object' &&
J
JJ Kasper 已提交
577
      typeof result.Component.renderReqToHTML === 'function'
J
JJ Kasper 已提交
578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610
    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 已提交
611 612
        isSprData ? 'application/json' : 'text/html; charset=utf-8',
        cachedData.curRevalidate
J
JJ Kasper 已提交
613 614 615 616 617 618
      )

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

J
JJ Kasper 已提交
621 622 623 624 625
    // If we're here, that means data is missing or it's stale.

    // Serverless requests need its URL transformed back into the original
    // request path (to emulate lambda behavior in production)
    if (isLikeServerless && isSprData) {
J
JJ Kasper 已提交
626 627 628
      let { pathname } = parseUrl(req.url || '', true)
      pathname = !pathname || pathname === '/' ? '/index' : pathname
      req.url = `/_next/data/${this.buildId}${pathname}.json`
J
JJ Kasper 已提交
629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660
    }

    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 }
661
    })
J
JJ Kasper 已提交
662 663 664 665 666 667 668 669

    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 已提交
670 671
            isSprData ? 'application/json' : 'text/html; charset=utf-8',
            sprRevalidate
J
JJ Kasper 已提交
672 673 674 675 676 677 678 679 680 681 682 683 684 685 686
          )
        }

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

        return null
      }
    )
687 688
  }

J
Joe Haddad 已提交
689
  public renderToHTML(
J
Joe Haddad 已提交
690 691 692 693
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
J
Joe Haddad 已提交
694 695 696 697 698 699 700
    {
      amphtml,
      dataOnly,
      hasAmp,
    }: {
      amphtml?: boolean
      hasAmp?: boolean
701 702
      dataOnly?: boolean
    } = {}
J
Joe Haddad 已提交
703
  ): Promise<string | null> {
J
Joe Haddad 已提交
704 705
    return this.findPageComponents(pathname, query)
      .then(
706
        result => {
J
Joe Haddad 已提交
707 708 709 710 711 712
          return this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
            query,
            result,
713
            { ...this.renderOpts, amphtml, hasAmp, dataOnly }
J
Joe Haddad 已提交
714 715
          )
        },
716
        err => {
J
Joe Haddad 已提交
717 718 719 720 721 722 723 724 725 726 727
          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(
728 729
              result => {
                return this.renderToHTMLWithComponents(
J
Joe Haddad 已提交
730 731 732
                  req,
                  res,
                  dynamicRoute.page,
J
JJ Kasper 已提交
733 734 735 736 737 738 739
                  // only add params for SPR enabled pages
                  {
                    ...(result.unstable_getStaticProps
                      ? { _nextSprData: query._nextSprData }
                      : query),
                    ...params,
                  },
J
Joe Haddad 已提交
740
                  result,
J
JJ Kasper 已提交
741 742 743 744 745 746
                  {
                    ...this.renderOpts,
                    amphtml,
                    hasAmp,
                    dataOnly,
                  }
747
                )
748
              }
J
Joe Haddad 已提交
749 750 751 752
            )
          }

          return Promise.reject(err)
753
        }
J
Joe Haddad 已提交
754
      )
755
      .catch(err => {
J
Joe Haddad 已提交
756 757 758 759 760 761 762 763 764
        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 已提交
765 766
  }

J
Joe Haddad 已提交
767 768 769 770 771
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
772
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
773 774 775
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
776
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
777
    )
N
Naoyuki Kanezawa 已提交
778
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
779
    if (html === null) {
780 781
      return
    }
782
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
783 784
  }

J
Joe Haddad 已提交
785 786 787 788 789
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
790
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
791
  ) {
J
Joe Haddad 已提交
792
    const result = await this.findPageComponents('/_error', query)
793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811
    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 已提交
812 813
  }

J
Joe Haddad 已提交
814 815 816
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
817
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
818
  ): Promise<void> {
819 820
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
821
    if (!pathname) {
822 823
      throw new Error('pathname is undefined')
    }
N
Naoyuki Kanezawa 已提交
824
    res.statusCode = 404
825
    return this.renderError(null, req, res, pathname, query)
N
Naoyuki Kanezawa 已提交
826
  }
N
Naoyuki Kanezawa 已提交
827

J
Joe Haddad 已提交
828 829 830 831
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
832
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
833
  ): Promise<void> {
A
Arunoda Susiripala 已提交
834
    if (!this.isServeableUrl(path)) {
835
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
836 837
    }

838 839 840 841 842 843
    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 已提交
844
    try {
845
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
846
    } catch (err) {
T
Tim Neutkens 已提交
847
      if (err.code === 'ENOENT' || err.statusCode === 404) {
848
        this.render404(req, res, parsedUrl)
849 850 851
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
852 853 854 855 856 857
      } else {
        throw err
      }
    }
  }

858
  private isServeableUrl(path: string): boolean {
A
Arunoda Susiripala 已提交
859 860
    const resolved = resolve(path)
    if (
861
      resolved.indexOf(join(this.distDir) + sep) !== 0 &&
862 863
      resolved.indexOf(join(this.dir, 'static') + sep) !== 0 &&
      resolved.indexOf(join(this.dir, 'public') + sep) !== 0
A
Arunoda Susiripala 已提交
864 865 866 867 868 869 870 871
    ) {
      // Seems like the user is trying to traverse the filesystem.
      return false
    }

    return true
  }

872
  protected readBuildId(): string {
873 874 875 876 877
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
878 879 880
        throw new Error(
          `Could not find a valid build in the '${
            this.distDir
881
          }' directory! Try building your app with 'next build' before starting the server.`
J
Joe Haddad 已提交
882
        )
883 884 885
      }

      throw err
886
    }
887
  }
888 889 890 891

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