next-server.ts 27.9 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'
6
import { parse as parseUrl, format as formatUrl, 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,
J
JJ Kasper 已提交
18
  ROUTES_MANIFEST,
19
  DEFAULT_REDIRECT_STATUS,
T
Tim Neutkens 已提交
20
} from '../lib/constants'
J
Joe Haddad 已提交
21 22 23 24
import {
  getRouteMatcher,
  getRouteRegex,
  getSortedRoutes,
25
  isDynamicRoute,
J
Joe Haddad 已提交
26
} from '../lib/router/utils'
27
import * as envConfig from '../lib/runtime-config'
J
Joe Haddad 已提交
28
import { isResSent, NextApiRequest, NextApiResponse } from '../lib/utils'
29
import { apiResolver } from './api-utils'
30
import loadConfig, { isTargetLikeServerless } from './config'
J
Joe Haddad 已提交
31
import { recursiveReadDirSync } from './lib/recursive-readdir-sync'
32
import { loadComponents, LoadComponentsReturnType } from './load-components'
J
Joe Haddad 已提交
33
import { renderToHTML } from './render'
J
Joe Haddad 已提交
34
import { getPagePath } from './require'
35
import Router, { Params, route, Route, RouteMatch } from './router'
J
Joe Haddad 已提交
36 37
import { sendHTML } from './send-html'
import { serveStatic } from './serve-static'
J
Joe Haddad 已提交
38
import { getSprCache, initializeSprCache, setSprCache } from './spr-cache'
J
Joe Haddad 已提交
39
import { isBlockedPage, isInternalUrl } from './utils'
J
JJ Kasper 已提交
40 41 42 43 44
import { fileExists } from '../../lib/file-exists'
import pathToRegexp from 'path-to-regexp'
import pathMatch from './lib/path-match'

const getCustomRouteMatcher = pathMatch(true)
45 46 47

type NextConfig = any

J
JJ Kasper 已提交
48 49 50 51 52 53 54 55 56
type Rewrite = {
  source: string
  destination: string
}

type Redirect = Rewrite & {
  statusCode?: number
}

57 58 59 60 61 62
type Middleware = (
  req: IncomingMessage,
  res: ServerResponse,
  next: (err?: Error) => void
) => void

T
Tim Neutkens 已提交
63
export type ServerConstructor = {
64 65 66
  /**
   * Where the Next project is located - @default '.'
   */
J
Joe Haddad 已提交
67 68
  dir?: string
  staticMarkup?: boolean
69 70 71
  /**
   * Hide error messages containing server information - @default false
   */
J
Joe Haddad 已提交
72
  quiet?: boolean
73 74 75
  /**
   * Object what you would use in next.config.js - @default {}
   */
76
  conf?: NextConfig
J
JJ Kasper 已提交
77
  dev?: boolean
78
}
79

N
nkzawa 已提交
80
export default class Server {
81 82 83 84
  dir: string
  quiet: boolean
  nextConfig: NextConfig
  distDir: string
85
  pagesDir?: string
86
  publicDir: string
J
JJ Kasper 已提交
87
  pagesManifest: string
88 89
  buildId: string
  renderOpts: {
T
Tim Neutkens 已提交
90
    poweredByHeader: boolean
T
Tim Neutkens 已提交
91
    ampBindInitData: boolean
J
Joe Haddad 已提交
92 93 94 95
    staticMarkup: boolean
    buildId: string
    generateEtags: boolean
    runtimeConfig?: { [key: string]: any }
96 97
    assetPrefix?: string
    canonicalBase: string
98
    documentMiddlewareEnabled: boolean
J
Joe Haddad 已提交
99
    hasCssMode: boolean
100
    dev?: boolean
101
  }
102
  private compression?: Middleware
103
  private onErrorMiddleware?: ({ err }: { err: Error }) => void
104
  router: Router
105
  protected dynamicRoutes?: Array<{ page: string; match: RouteMatch }>
J
JJ Kasper 已提交
106 107 108 109
  protected customRoutes?: {
    rewrites: Rewrite[]
    redirects: Redirect[]
  }
110

J
Joe Haddad 已提交
111 112 113 114 115
  public constructor({
    dir = '.',
    staticMarkup = false,
    quiet = false,
    conf = null,
J
JJ Kasper 已提交
116
    dev = false,
J
Joe Haddad 已提交
117
  }: ServerConstructor = {}) {
N
nkzawa 已提交
118
    this.dir = resolve(dir)
N
Naoyuki Kanezawa 已提交
119
    this.quiet = quiet
T
Tim Neutkens 已提交
120
    const phase = this.currentPhase()
121
    this.nextConfig = loadConfig(phase, this.dir, conf)
122
    this.distDir = join(this.dir, this.nextConfig.distDir)
123
    this.publicDir = join(this.dir, CLIENT_PUBLIC_FILES_PATH)
J
JJ Kasper 已提交
124 125
    this.pagesManifest = join(
      this.distDir,
126 127 128
      this.nextConfig.target === 'server'
        ? SERVER_DIRECTORY
        : SERVERLESS_DIRECTORY,
J
JJ Kasper 已提交
129 130
      PAGES_MANIFEST
    )
T
Tim Neutkens 已提交
131

132 133
    // Only serverRuntimeConfig needs the default
    // publicRuntimeConfig gets it's default in client/index.js
J
Joe Haddad 已提交
134 135 136 137 138
    const {
      serverRuntimeConfig = {},
      publicRuntimeConfig,
      assetPrefix,
      generateEtags,
139
      compress,
J
Joe Haddad 已提交
140
    } = this.nextConfig
141

T
Tim Neutkens 已提交
142
    this.buildId = this.readBuildId()
143

144
    this.renderOpts = {
T
Tim Neutkens 已提交
145
      ampBindInitData: this.nextConfig.experimental.ampBindInitData,
T
Tim Neutkens 已提交
146
      poweredByHeader: this.nextConfig.poweredByHeader,
147
      canonicalBase: this.nextConfig.amp.canonicalBase,
148 149
      documentMiddlewareEnabled: this.nextConfig.experimental
        .documentMiddleware,
J
Joe Haddad 已提交
150
      hasCssMode: this.nextConfig.experimental.css,
151
      staticMarkup,
152
      buildId: this.buildId,
153
      generateEtags,
154
    }
N
Naoyuki Kanezawa 已提交
155

156 157
    // Only the `publicRuntimeConfig` key is exposed to the client side
    // It'll be rendered as part of __NEXT_DATA__ on the client side
158
    if (Object.keys(publicRuntimeConfig).length > 0) {
159
      this.renderOpts.runtimeConfig = publicRuntimeConfig
160 161
    }

162
    if (compress && this.nextConfig.target === 'server') {
163 164 165
      this.compression = compression() as Middleware
    }

166
    // Initialize next/config with the environment configuration
167 168 169 170 171 172
    if (this.nextConfig.target === 'server') {
      envConfig.setConfig({
        serverRuntimeConfig,
        publicRuntimeConfig,
      })
    }
173

J
JJ Kasper 已提交
174
    this.router = new Router(this.generateRoutes())
175
    this.setAssetPrefix(assetPrefix)
J
JJ Kasper 已提交
176

177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
    // call init-server middleware, this is also handled
    // individually in serverless bundles when deployed
    if (!dev && this.nextConfig.experimental.plugins) {
      const serverPath = join(
        this.distDir,
        this._isLikeServerless ? 'serverless' : 'server'
      )
      const initServer = require(join(serverPath, 'init-server.js')).default
      this.onErrorMiddleware = require(join(
        serverPath,
        'on-error-server.js'
      )).default
      initServer()
    }

J
JJ Kasper 已提交
192 193 194 195 196 197 198 199 200 201 202 203
    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 已提交
204
  }
N
nkzawa 已提交
205

206
  protected currentPhase(): string {
207
    return PHASE_PRODUCTION_SERVER
208 209
  }

210 211 212 213
  private logError(err: Error): void {
    if (this.onErrorMiddleware) {
      this.onErrorMiddleware({ err })
    }
214 215
    if (this.quiet) return
    // tslint:disable-next-line
216
    console.error(err)
217 218
  }

J
Joe Haddad 已提交
219 220 221
  private handleRequest(
    req: IncomingMessage,
    res: ServerResponse,
222
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
223
  ): Promise<void> {
224
    // Parse url if parsedUrl not provided
225
    if (!parsedUrl || typeof parsedUrl !== 'object') {
226 227
      const url: any = req.url
      parsedUrl = parseUrl(url, true)
228
    }
229

230 231 232
    // 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 已提交
233
    }
234

235
    res.statusCode = 200
236
    return this.run(req, res, parsedUrl).catch(err => {
J
Joe Haddad 已提交
237 238 239 240
      this.logError(err)
      res.statusCode = 500
      res.end('Internal Server Error')
    })
241 242
  }

243
  public getRequestHandler() {
244
    return this.handleRequest.bind(this)
N
nkzawa 已提交
245 246
  }

247
  public setAssetPrefix(prefix?: string) {
248
    this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
249 250
  }

251
  // Backwards compatibility
252
  public async prepare(): Promise<void> {}
N
nkzawa 已提交
253

T
Tim Neutkens 已提交
254
  // Backwards compatibility
255
  protected async close(): Promise<void> {}
T
Tim Neutkens 已提交
256

257
  protected setImmutableAssetCacheControl(res: ServerResponse) {
T
Tim Neutkens 已提交
258
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
N
nkzawa 已提交
259 260
  }

J
JJ Kasper 已提交
261 262 263 264
  protected getCustomRoutes() {
    return require(join(this.distDir, ROUTES_MANIFEST))
  }

265
  protected generateRoutes(): Route[] {
J
JJ Kasper 已提交
266 267
    this.customRoutes = this.getCustomRoutes()

268 269 270
    const publicRoutes = fs.existsSync(this.publicDir)
      ? this.generatePublicRoutes()
      : []
J
JJ Kasper 已提交
271

272 273 274 275 276 277 278 279 280 281 282 283 284 285 286
    const staticFilesRoute = fs.existsSync(join(this.dir, 'static'))
      ? [
          {
            // 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.
            // See more: https://github.com/zeit/next.js/issues/2617
            match: route('/static/:path*'),
            fn: async (req, res, params, parsedUrl) => {
              const p = join(this.dir, 'static', ...(params.path || []))
              await this.serveStatic(req, res, p, parsedUrl)
            },
          } as Route,
        ]
      : []
287

288
    const routes: Route[] = [
T
Tim Neutkens 已提交
289
      {
290
        match: route('/_next/static/:path*'),
291
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
292 293 294
          // 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.
295 296 297 298

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

J
Joe Haddad 已提交
299 300 301 302 303
          if (
            params.path[0] === CLIENT_STATIC_FILES_RUNTIME ||
            params.path[0] === 'chunks' ||
            params.path[0] === this.buildId
          ) {
T
Tim Neutkens 已提交
304
            this.setImmutableAssetCacheControl(res)
305
          }
J
Joe Haddad 已提交
306 307 308
          const p = join(
            this.distDir,
            CLIENT_STATIC_FILES_PATH,
309
            ...(params.path || [])
J
Joe Haddad 已提交
310
          )
311
          await this.serveStatic(req, res, p, parsedUrl)
312
        },
313
      },
J
JJ Kasper 已提交
314 315 316
      {
        match: route('/_next/data/:path*'),
        fn: async (req, res, params, _parsedUrl) => {
J
JJ Kasper 已提交
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
          // 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 已提交
335 336 337 338 339 340 341 342 343 344 345
          req.url = pathname
          const parsedUrl = parseUrl(pathname, true)
          await this.render(
            req,
            res,
            pathname,
            { _nextSprData: '1' },
            parsedUrl
          )
        },
      },
T
Tim Neutkens 已提交
346
      {
347
        match: route('/_next/:path*'),
T
Tim Neutkens 已提交
348
        // This path is needed because `render()` does a check for `/_next` and the calls the routing again
349
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
350
          await this.render404(req, res, parsedUrl)
351
        },
T
Tim Neutkens 已提交
352
      },
353
      ...publicRoutes,
354
      ...staticFilesRoute,
L
Lukáš Huvar 已提交
355 356 357 358
      {
        match: route('/api/:path*'),
        fn: async (req, res, params, parsedUrl) => {
          const { pathname } = parsedUrl
L
Lukáš Huvar 已提交
359 360 361 362 363
          await this.handleApiRequest(
            req as NextApiRequest,
            res as NextApiResponse,
            pathname!
          )
L
Lukáš Huvar 已提交
364 365
        },
      },
T
Tim Neutkens 已提交
366
    ]
367

J
JJ Kasper 已提交
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418
    if (this.customRoutes) {
      const { redirects, rewrites } = this.customRoutes

      const getCustomRoute = (
        r: { source: string; destination: string; statusCode?: number },
        type: 'redirect' | 'rewrite'
      ) => ({
        ...r,
        type,
        matcher: getCustomRouteMatcher(r.source),
      })

      const customRoutes = [
        ...redirects.map(r => getCustomRoute(r, 'redirect')),
        ...rewrites.map(r => getCustomRoute(r, 'rewrite')),
      ]

      routes.push(
        ...customRoutes.map((r, idx) => {
          return {
            match: r.matcher,
            fn: async (req, res, params, parsedUrl) => {
              let destinationCompiler = pathToRegexp.compile(r.destination)
              let newUrl = destinationCompiler(params) // /blog/123
              let newParams = params // { id: 123 }
              let statusCode = r.statusCode
              const followingRoutes = customRoutes.slice(idx + 1)

              for (const followingRoute of followingRoutes) {
                if (
                  r.type === 'redirect' &&
                  followingRoute.type !== 'redirect'
                ) {
                  continue
                }

                // TODO: add an error if they try to rewrite to a dynamic page
                const curParams = followingRoute.matcher(newUrl)

                if (curParams) {
                  destinationCompiler = pathToRegexp.compile(
                    followingRoute.destination
                  )
                  newUrl = destinationCompiler(newParams)
                  statusCode = followingRoute.statusCode
                  newParams = { ...newParams, ...curParams }
                }
              }

              if (r.type === 'redirect') {
                res.setHeader('Location', newUrl)
419
                res.statusCode = statusCode || DEFAULT_REDIRECT_STATUS
J
JJ Kasper 已提交
420 421 422 423 424 425 426 427 428 429
                res.end()
                return
              }
              return this.render(req, res, newUrl, newParams, parsedUrl)
            },
          } as Route
        })
      )
    }

430
    if (this.nextConfig.useFileSystemPublicRoutes) {
J
Joe Haddad 已提交
431
      this.dynamicRoutes = this.getDynamicRoutes()
J
Joe Haddad 已提交
432

433 434 435
      // 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.
436
      // See more: https://github.com/zeit/next.js/issues/2617
T
Tim Neutkens 已提交
437
      routes.push({
438
        match: route('/:path*'),
439
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
440
          const { pathname, query } = parsedUrl
441
          if (!pathname) {
442 443
            throw new Error('pathname is undefined')
          }
444

445
          await this.render(req, res, pathname, query, parsedUrl)
446
        },
T
Tim Neutkens 已提交
447
      })
448
    }
N
nkzawa 已提交
449

T
Tim Neutkens 已提交
450 451 452
    return routes
  }

L
Lukáš Huvar 已提交
453 454 455 456 457 458
  /**
   * Resolves `API` request, in development builds on demand
   * @param req http request
   * @param res http response
   * @param pathname path of request
   */
J
Joe Haddad 已提交
459
  private async handleApiRequest(
L
Lukáš Huvar 已提交
460 461
    req: NextApiRequest,
    res: NextApiResponse,
462
    pathname: string
J
Joe Haddad 已提交
463
  ) {
L
Lukáš Huvar 已提交
464
    let params: Params | boolean = false
J
JJ Kasper 已提交
465 466 467 468 469
    let resolverFunction: any

    try {
      resolverFunction = await this.resolveApiRequest(pathname)
    } catch (err) {}
L
Lukáš Huvar 已提交
470 471 472 473 474 475 476 477 478 479 480 481 482 483 484

    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 已提交
485 486 487 488
    if (!resolverFunction) {
      return this.render404(req, res)
    }

489
    if (!this.renderOpts.dev && this._isLikeServerless) {
J
JJ Kasper 已提交
490 491 492 493 494 495
      const mod = require(resolverFunction)
      if (typeof mod.default === 'function') {
        return mod.default(req, res)
      }
    }

496
    await apiResolver(
497 498 499 500 501
      req,
      res,
      params,
      resolverFunction ? require(resolverFunction) : undefined
    )
L
Lukáš Huvar 已提交
502 503 504 505 506 507
  }

  /**
   * Resolves path to resolver function
   * @param pathname path of request
   */
508
  protected async resolveApiRequest(pathname: string): Promise<string | null> {
J
Joe Haddad 已提交
509 510 511
    return getPagePath(
      pathname,
      this.distDir,
512
      this._isLikeServerless,
513
      this.renderOpts.dev
J
Joe Haddad 已提交
514
    )
L
Lukáš Huvar 已提交
515 516
  }

517
  protected generatePublicRoutes(): Route[] {
518 519
    const routes: Route[] = []
    const publicFiles = recursiveReadDirSync(this.publicDir)
520 521
    const serverBuildPath = join(
      this.distDir,
522
      this._isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY
523
    )
524 525
    const pagesManifest = require(join(serverBuildPath, PAGES_MANIFEST))

526
    publicFiles.forEach(path => {
527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542
      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
  }

543
  protected getDynamicRoutes() {
J
JJ Kasper 已提交
544 545
    const manifest = require(this.pagesManifest)
    const dynamicRoutedPages = Object.keys(manifest).filter(isDynamicRoute)
546 547 548 549
    return getSortedRoutes(dynamicRoutedPages).map(page => ({
      page,
      match: getRouteMatcher(getRouteRegex(page)),
    }))
J
Joe Haddad 已提交
550 551
  }

552 553 554 555 556 557
  private handleCompression(req: IncomingMessage, res: ServerResponse) {
    if (this.compression) {
      this.compression(req, res, () => {})
    }
  }

558
  protected async run(
J
Joe Haddad 已提交
559 560
    req: IncomingMessage,
    res: ServerResponse,
561
    parsedUrl: UrlWithParsedQuery
J
Joe Haddad 已提交
562
  ) {
563 564
    this.handleCompression(req, res)

565 566 567 568 569 570 571 572 573 574 575 576
    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
577 578
    }

579
    await this.render404(req, res, parsedUrl)
N
nkzawa 已提交
580 581
  }

582
  protected async sendHTML(
J
Joe Haddad 已提交
583 584
    req: IncomingMessage,
    res: ServerResponse,
585
    html: string
J
Joe Haddad 已提交
586
  ) {
T
Tim Neutkens 已提交
587 588
    const { generateEtags, poweredByHeader } = this.renderOpts
    return sendHTML(req, res, html, { generateEtags, poweredByHeader })
589 590
  }

J
Joe Haddad 已提交
591 592 593 594 595
  public async render(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
596
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
597
  ): Promise<void> {
598 599
    const url: any = req.url
    if (isInternalUrl(url)) {
600 601 602
      return this.handleRequest(req, res, parsedUrl)
    }

603
    if (isBlockedPage(pathname)) {
604
      return this.render404(req, res, parsedUrl)
605 606
    }

607
    const html = await this.renderToHTML(req, res, pathname, query, {
J
Joe Haddad 已提交
608 609 610 611 612
      dataOnly:
        (this.renderOpts.ampBindInitData && Boolean(query.dataOnly)) ||
        (req.headers &&
          (req.headers.accept || '').indexOf('application/amp.bind+json') !==
            -1),
613
    })
614 615
    // Request was ended by the user
    if (html === null) {
616 617 618
      return
    }

619
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
620
  }
N
nkzawa 已提交
621

J
Joe Haddad 已提交
622
  private async findPageComponents(
J
Joe Haddad 已提交
623
    pathname: string,
624
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
625
  ) {
626
    const serverless = !this.renderOpts.dev && this._isLikeServerless
J
JJ Kasper 已提交
627 628 629
    // try serving a static AMP version first
    if (query.amp) {
      try {
J
Joe Haddad 已提交
630 631 632 633
        return await loadComponents(
          this.distDir,
          this.buildId,
          (pathname === '/' ? '/index' : pathname) + '.amp',
634
          serverless
J
Joe Haddad 已提交
635
        )
J
JJ Kasper 已提交
636 637 638 639
      } catch (err) {
        if (err.code !== 'ENOENT') throw err
      }
    }
J
Joe Haddad 已提交
640 641 642 643
    return await loadComponents(
      this.distDir,
      this.buildId,
      pathname,
644
      serverless
J
Joe Haddad 已提交
645 646 647
    )
  }

J
JJ Kasper 已提交
648 649 650 651 652 653
  private __sendPayload(
    res: ServerResponse,
    payload: any,
    type: string,
    revalidate?: number | false
  ) {
J
JJ Kasper 已提交
654
    // TODO: ETag? Cache-Control headers? Next-specific headers?
J
JJ Kasper 已提交
655
    res.setHeader('Content-Type', type)
J
JJ Kasper 已提交
656
    res.setHeader('Content-Length', Buffer.byteLength(payload))
J
Joe Haddad 已提交
657 658 659 660 661 662 663 664 665 666 667 668
    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 已提交
669
    }
J
JJ Kasper 已提交
670 671 672
    res.end(payload)
  }

J
Joe Haddad 已提交
673 674 675 676 677 678
  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
    result: LoadComponentsReturnType,
679
    opts: any
J
JJ Kasper 已提交
680
  ): Promise<string | null> {
J
JJ Kasper 已提交
681
    // handle static page
J
Joe Haddad 已提交
682 683 684 685
    if (typeof result.Component === 'string') {
      return result.Component
    }

J
JJ Kasper 已提交
686 687
    // check request state
    const isLikeServerless =
J
Joe Haddad 已提交
688
      typeof result.Component === 'object' &&
J
JJ Kasper 已提交
689
      typeof result.Component.renderReqToHTML === 'function'
J
JJ Kasper 已提交
690 691 692 693 694 695
    const isSpr = !!result.unstable_getStaticProps

    // non-spr requests should render like normal
    if (!isSpr) {
      // handle serverless
      if (isLikeServerless) {
696 697 698 699 700 701 702 703
        const curUrl = parseUrl(req.url!, true)
        req.url = formatUrl({
          ...curUrl,
          query: {
            ...curUrl.query,
            ...query,
          },
        })
J
JJ Kasper 已提交
704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730
        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 已提交
731 732
        isSprData ? 'application/json' : 'text/html; charset=utf-8',
        cachedData.curRevalidate
J
JJ Kasper 已提交
733 734 735 736 737 738
      )

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

J
JJ Kasper 已提交
741 742 743 744 745
    // 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 已提交
746 747 748
      let { pathname } = parseUrl(req.url || '', true)
      pathname = !pathname || pathname === '/' ? '/index' : pathname
      req.url = `/_next/data/${this.buildId}${pathname}.json`
J
JJ Kasper 已提交
749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780
    }

    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 }
781
    })
J
JJ Kasper 已提交
782 783 784 785 786 787 788 789

    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 已提交
790 791
            isSprData ? 'application/json' : 'text/html; charset=utf-8',
            sprRevalidate
J
JJ Kasper 已提交
792 793 794 795 796 797 798 799 800 801 802 803 804 805 806
          )
        }

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

        return null
      }
    )
807 808
  }

J
Joe Haddad 已提交
809
  public renderToHTML(
J
Joe Haddad 已提交
810 811 812 813
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
J
Joe Haddad 已提交
814 815 816 817 818 819 820
    {
      amphtml,
      dataOnly,
      hasAmp,
    }: {
      amphtml?: boolean
      hasAmp?: boolean
821 822
      dataOnly?: boolean
    } = {}
J
Joe Haddad 已提交
823
  ): Promise<string | null> {
J
Joe Haddad 已提交
824 825
    return this.findPageComponents(pathname, query)
      .then(
826
        result => {
J
Joe Haddad 已提交
827 828 829 830 831 832
          return this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
            query,
            result,
833
            { ...this.renderOpts, amphtml, hasAmp, dataOnly }
J
Joe Haddad 已提交
834 835
          )
        },
836
        err => {
J
Joe Haddad 已提交
837 838 839 840 841 842 843 844 845 846 847
          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(
848 849
              result => {
                return this.renderToHTMLWithComponents(
J
Joe Haddad 已提交
850 851 852
                  req,
                  res,
                  dynamicRoute.page,
J
JJ Kasper 已提交
853 854 855 856 857 858 859
                  // only add params for SPR enabled pages
                  {
                    ...(result.unstable_getStaticProps
                      ? { _nextSprData: query._nextSprData }
                      : query),
                    ...params,
                  },
J
Joe Haddad 已提交
860
                  result,
J
JJ Kasper 已提交
861 862 863 864 865 866
                  {
                    ...this.renderOpts,
                    amphtml,
                    hasAmp,
                    dataOnly,
                  }
867
                )
868
              }
J
Joe Haddad 已提交
869 870 871 872
            )
          }

          return Promise.reject(err)
873
        }
J
Joe Haddad 已提交
874
      )
875
      .catch(err => {
J
Joe Haddad 已提交
876 877 878 879 880 881 882 883 884
        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 已提交
885 886
  }

J
Joe Haddad 已提交
887 888 889 890 891
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
892
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
893 894 895
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
896
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
897
    )
N
Naoyuki Kanezawa 已提交
898
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
899
    if (html === null) {
900 901
      return
    }
902
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
903 904
  }

J
Joe Haddad 已提交
905 906 907 908 909
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
910
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
911
  ) {
J
Joe Haddad 已提交
912
    const result = await this.findPageComponents('/_error', query)
913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931
    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 已提交
932 933
  }

J
Joe Haddad 已提交
934 935 936
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
937
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
938
  ): Promise<void> {
939 940
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
941
    if (!pathname) {
942 943
      throw new Error('pathname is undefined')
    }
N
Naoyuki Kanezawa 已提交
944
    res.statusCode = 404
945
    return this.renderError(null, req, res, pathname, query)
N
Naoyuki Kanezawa 已提交
946
  }
N
Naoyuki Kanezawa 已提交
947

J
Joe Haddad 已提交
948 949 950 951
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
952
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
953
  ): Promise<void> {
A
Arunoda Susiripala 已提交
954
    if (!this.isServeableUrl(path)) {
955
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
956 957
    }

958 959 960 961 962 963
    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 已提交
964
    try {
965
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
966
    } catch (err) {
T
Tim Neutkens 已提交
967
      if (err.code === 'ENOENT' || err.statusCode === 404) {
968
        this.render404(req, res, parsedUrl)
969 970 971
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
972 973 974 975 976 977
      } else {
        throw err
      }
    }
  }

978
  private isServeableUrl(path: string): boolean {
A
Arunoda Susiripala 已提交
979 980
    const resolved = resolve(path)
    if (
981
      resolved.indexOf(join(this.distDir) + sep) !== 0 &&
982 983
      resolved.indexOf(join(this.dir, 'static') + sep) !== 0 &&
      resolved.indexOf(join(this.dir, 'public') + sep) !== 0
A
Arunoda Susiripala 已提交
984 985 986 987 988 989 990 991
    ) {
      // Seems like the user is trying to traverse the filesystem.
      return false
    }

    return true
  }

992
  protected readBuildId(): string {
993 994 995 996 997
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
998 999 1000
        throw new Error(
          `Could not find a valid build in the '${
            this.distDir
1001
          }' directory! Try building your app with 'next build' before starting the server.`
J
Joe Haddad 已提交
1002
        )
1003 1004 1005
      }

      throw err
1006
    }
1007
  }
1008 1009 1010 1011

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