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
J
JJ Kasper 已提交
103
  private onErrorMiddleware?: ({ err }: { err: Error }) => Promise<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
      req,
      res,
      params,
J
JJ Kasper 已提交
501 502
      resolverFunction ? require(resolverFunction) : undefined,
      this.onErrorMiddleware
503
    )
L
Lukáš Huvar 已提交
504 505 506 507 508 509
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    // non-spr requests should render like normal
    if (!isSpr) {
      // handle serverless
      if (isLikeServerless) {
702 703 704 705 706 707 708 709
        const curUrl = parseUrl(req.url!, true)
        req.url = formatUrl({
          ...curUrl,
          query: {
            ...curUrl.query,
            ...query,
          },
        })
J
JJ Kasper 已提交
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 736
        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 已提交
737 738
        isSprData ? 'application/json' : 'text/html; charset=utf-8',
        cachedData.curRevalidate
J
JJ Kasper 已提交
739 740 741 742 743 744
      )

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

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

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

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

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

        return null
      }
    )
813 814
  }

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

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

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

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

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

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

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

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

    return true
  }

998
  protected readBuildId(): string {
999 1000 1001 1002 1003
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
1004
        throw new Error(
1005
          `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 已提交
1006
        )
1007 1008 1009
      }

      throw err
1010
    }
1011
  }
1012 1013 1014 1015

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