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
    // 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
JJ Kasper 已提交
545 546 547 548

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

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

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

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

J
JJ Kasper 已提交
615 616 617 618 619
    // 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 已提交
620 621 622
      let { pathname } = parseUrl(req.url || '', true)
      pathname = !pathname || pathname === '/' ? '/index' : pathname
      req.url = `/_next/data/${this.buildId}${pathname}.json`
J
JJ Kasper 已提交
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 653 654
    }

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

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

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

        return null
      }
    )
681 682
  }

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

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

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

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

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

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

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

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

    return true
  }

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

      throw err
880
    }
881
  }
882 883 884 885

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