next-server.ts 28.0 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 pathToRegexp from 'path-to-regexp'
6
import { parse as parseQs, ParsedUrlQuery } from 'querystring'
7
import { format as formatUrl, parse as parseUrl, UrlWithParsedQuery } from 'url'
J
Joe Haddad 已提交
8

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

const getCustomRouteMatcher = pathMatch(true)
44 45 46

type NextConfig = any

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

type Redirect = Rewrite & {
  statusCode?: number
}

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

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

N
nkzawa 已提交
79
export default class Server {
80 81 82 83
  dir: string
  quiet: boolean
  nextConfig: NextConfig
  distDir: string
84
  pagesDir?: string
85
  publicDir: string
86
  hasStaticDir: boolean
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)
124
    this.hasStaticDir = fs.existsSync(join(this.dir, 'static'))
J
JJ Kasper 已提交
125 126
    this.pagesManifest = join(
      this.distDir,
127 128 129
      this.nextConfig.target === 'server'
        ? SERVER_DIRECTORY
        : SERVERLESS_DIRECTORY,
J
JJ Kasper 已提交
130 131
      PAGES_MANIFEST
    )
T
Tim Neutkens 已提交
132

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

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

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

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

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

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

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

178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
    // 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 已提交
193 194 195 196 197 198 199 200 201 202 203 204
    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 已提交
205
  }
N
nkzawa 已提交
206

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

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

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

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

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

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

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

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

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

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

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

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

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

273
    const staticFilesRoute = this.hasStaticDir
274 275 276 277 278 279 280 281 282 283 284 285 286 287
      ? [
          {
            // 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,
        ]
      : []
288

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

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

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

J
JJ Kasper 已提交
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 419
    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)
420
                res.statusCode = statusCode || DEFAULT_REDIRECT_STATUS
J
JJ Kasper 已提交
421 422 423 424 425 426 427 428 429 430
                res.end()
                return
              }
              return this.render(req, res, newUrl, newParams, parsedUrl)
            },
          } as Route
        })
      )
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    if (
      url.match(/^\/_next\//) ||
      (this.hasStaticDir && url.match(/^\/static\//))
    ) {
605 606 607
      return this.handleRequest(req, res, parsedUrl)
    }

608
    if (isBlockedPage(pathname)) {
609
      return this.render404(req, res, parsedUrl)
610 611
    }

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

624
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
625
  }
N
nkzawa 已提交
626

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

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

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

J
JJ Kasper 已提交
691 692
    // check request state
    const isLikeServerless =
J
Joe Haddad 已提交
693
      typeof result.Component === 'object' &&
J
JJ Kasper 已提交
694
      typeof result.Component.renderReqToHTML === 'function'
J
JJ Kasper 已提交
695 696 697 698 699 700
    const isSpr = !!result.unstable_getStaticProps

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

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

J
JJ Kasper 已提交
746 747 748 749 750
    // 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 已提交
751 752 753
      let { pathname } = parseUrl(req.url || '', true)
      pathname = !pathname || pathname === '/' ? '/index' : pathname
      req.url = `/_next/data/${this.buildId}${pathname}.json`
J
JJ Kasper 已提交
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 781 782 783 784 785
    }

    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 }
786
    })
J
JJ Kasper 已提交
787 788 789 790 791 792 793 794

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

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

        return null
      }
    )
812 813
  }

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

          return Promise.reject(err)
878
        }
J
Joe Haddad 已提交
879
      )
880
      .catch(err => {
J
Joe Haddad 已提交
881 882 883 884 885 886 887 888 889
        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 已提交
890 891
  }

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

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

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

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

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

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

    return true
  }

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

      throw err
1009
    }
1010
  }
1011 1012 1013 1014

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