next-server.ts 24.2 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 223 224 225
    const publicRoutes = fs.existsSync(this.publicDir)
      ? this.generatePublicRoutes()
      : []

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

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

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

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

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

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

T
Tim Neutkens 已提交
336 337 338
    return routes
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

J
JJ Kasper 已提交
534 535 536 537 538 539
  private __sendPayload(
    res: ServerResponse,
    payload: any,
    type: string,
    revalidate?: number | false
  ) {
J
JJ Kasper 已提交
540
    // TODO: ETag? Cache-Control headers? Next-specific headers?
J
JJ Kasper 已提交
541
    res.setHeader('Content-Type', type)
J
JJ Kasper 已提交
542
    res.setHeader('Content-Length', Buffer.byteLength(payload))
J
JJ Kasper 已提交
543 544 545 546

    if (revalidate) {
      res.setHeader(
        'Cache-Control',
547
        `s-maxage=${revalidate}, stale-while-revalidate`
J
JJ Kasper 已提交
548 549
      )
    }
J
JJ Kasper 已提交
550 551 552
    res.end(payload)
  }

J
Joe Haddad 已提交
553 554 555 556 557 558
  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
    result: LoadComponentsReturnType,
559
    opts: any
J
JJ Kasper 已提交
560
  ): Promise<string | null> {
J
JJ Kasper 已提交
561
    // handle static page
J
Joe Haddad 已提交
562 563 564 565
    if (typeof result.Component === 'string') {
      return result.Component
    }

J
JJ Kasper 已提交
566 567
    // check request state
    const isLikeServerless =
J
Joe Haddad 已提交
568
      typeof result.Component === 'object' &&
J
JJ Kasper 已提交
569
      typeof result.Component.renderReqToHTML === 'function'
J
JJ Kasper 已提交
570 571 572 573 574 575 576 577 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
    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 已提交
603 604
        isSprData ? 'application/json' : 'text/html; charset=utf-8',
        cachedData.curRevalidate
J
JJ Kasper 已提交
605 606 607 608 609 610
      )

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

J
JJ Kasper 已提交
613 614 615 616 617
    // 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 已提交
618 619 620
      let { pathname } = parseUrl(req.url || '', true)
      pathname = !pathname || pathname === '/' ? '/index' : pathname
      req.url = `/_next/data/${this.buildId}${pathname}.json`
J
JJ Kasper 已提交
621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652
    }

    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 }
653
    })
J
JJ Kasper 已提交
654 655 656 657 658 659 660 661

    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 已提交
662 663
            isSprData ? 'application/json' : 'text/html; charset=utf-8',
            sprRevalidate
J
JJ Kasper 已提交
664 665 666 667 668 669 670 671 672 673 674 675 676 677 678
          )
        }

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

        return null
      }
    )
679 680
  }

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

          return Promise.reject(err)
745
        }
J
Joe Haddad 已提交
746
      )
747
      .catch(err => {
J
Joe Haddad 已提交
748 749 750 751 752 753 754 755 756
        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 已提交
757 758
  }

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

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

J
Joe Haddad 已提交
806 807 808
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
809
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
810
  ): Promise<void> {
811 812
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
813
    if (!pathname) {
814 815
      throw new Error('pathname is undefined')
    }
N
Naoyuki Kanezawa 已提交
816
    res.statusCode = 404
817
    return this.renderError(null, req, res, pathname, query)
N
Naoyuki Kanezawa 已提交
818
  }
N
Naoyuki Kanezawa 已提交
819

J
Joe Haddad 已提交
820 821 822 823
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
824
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
825
  ): Promise<void> {
A
Arunoda Susiripala 已提交
826
    if (!this.isServeableUrl(path)) {
827
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
828 829
    }

830 831 832 833 834 835
    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 已提交
836
    try {
837
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
838
    } catch (err) {
T
Tim Neutkens 已提交
839
      if (err.code === 'ENOENT' || err.statusCode === 404) {
840
        this.render404(req, res, parsedUrl)
841 842 843
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
844 845 846 847 848 849
      } else {
        throw err
      }
    }
  }

850
  private isServeableUrl(path: string): boolean {
A
Arunoda Susiripala 已提交
851 852
    const resolved = resolve(path)
    if (
853
      resolved.indexOf(join(this.distDir) + sep) !== 0 &&
854 855
      resolved.indexOf(join(this.dir, 'static') + sep) !== 0 &&
      resolved.indexOf(join(this.dir, 'public') + sep) !== 0
A
Arunoda Susiripala 已提交
856 857 858 859 860 861 862 863
    ) {
      // Seems like the user is trying to traverse the filesystem.
      return false
    }

    return true
  }

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

      throw err
878
    }
879
  }
880 881 882 883

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