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 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'
J
Joe Haddad 已提交
41
import { isBlockedPage, isInternalUrl } 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
J
JJ Kasper 已提交
86
  pagesManifest: string
87 88
  buildId: string
  renderOpts: {
T
Tim Neutkens 已提交
89
    poweredByHeader: boolean
T
Tim Neutkens 已提交
90
    ampBindInitData: boolean
J
Joe Haddad 已提交
91 92 93 94
    staticMarkup: boolean
    buildId: string
    generateEtags: boolean
    runtimeConfig?: { [key: string]: any }
95 96
    assetPrefix?: string
    canonicalBase: string
97
    documentMiddlewareEnabled: boolean
J
Joe Haddad 已提交
98
    hasCssMode: boolean
99
    dev?: boolean
100
  }
101
  private compression?: Middleware
102
  private onErrorMiddleware?: ({ err }: { err: Error }) => void
103
  router: Router
104
  protected dynamicRoutes?: Array<{ page: string; match: RouteMatch }>
J
JJ Kasper 已提交
105 106 107 108
  protected customRoutes?: {
    rewrites: Rewrite[]
    redirects: Redirect[]
  }
109

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
    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,
        ]
      : []
286

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    // non-spr requests should render like normal
    if (!isSpr) {
      // handle serverless
      if (isLikeServerless) {
695 696 697 698 699 700 701 702
        const curUrl = parseUrl(req.url!, true)
        req.url = formatUrl({
          ...curUrl,
          query: {
            ...curUrl.query,
            ...query,
          },
        })
J
JJ Kasper 已提交
703 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
        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 已提交
730 731
        isSprData ? 'application/json' : 'text/html; charset=utf-8',
        cachedData.curRevalidate
J
JJ Kasper 已提交
732 733 734 735 736 737
      )

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

J
JJ Kasper 已提交
740 741 742 743 744
    // 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 已提交
745 746 747
      let { pathname } = parseUrl(req.url || '', true)
      pathname = !pathname || pathname === '/' ? '/index' : pathname
      req.url = `/_next/data/${this.buildId}${pathname}.json`
J
JJ Kasper 已提交
748 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
    }

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

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

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

        return null
      }
    )
806 807
  }

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

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

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

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

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

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

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

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

    return true
  }

991
  protected readBuildId(): string {
992 993 994 995 996
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
997
        throw new Error(
998
          `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 已提交
999
        )
1000 1001 1002
      }

      throw err
1003
    }
1004
  }
1005 1006 1007 1008

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