next-server.ts 27.7 KB
Newer Older
1
import compression from 'compression'
J
Joe Haddad 已提交
2
import fs from 'fs'
J
Joe Haddad 已提交
3
import { IncomingMessage, ServerResponse } from 'http'
J
Joe Haddad 已提交
4
import { join, resolve, sep } from 'path'
5
import { parse as parseQs, ParsedUrlQuery } from 'querystring'
J
Joe Haddad 已提交
6
import { parse as parseUrl, UrlWithParsedQuery } from 'url'
J
Joe Haddad 已提交
7

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

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 418 419 420 421 422 423 424 425 426 427 428
    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)
                res.statusCode = statusCode || 307
                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 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721
    const isSpr = !!result.unstable_getStaticProps

    // non-spr requests should render like normal
    if (!isSpr) {
      // handle serverless
      if (isLikeServerless) {
        return result.Component.renderReqToHTML(req, res)
      }

      return renderToHTML(req, res, pathname, query, {
        ...result,
        ...opts,
      })
    }

    // Toggle whether or not this is an SPR Data request
    const isSprData = isSpr && query._nextSprData
    if (isSprData) {
      delete query._nextSprData
    }
    // Compute the SPR cache key
    const sprCacheKey = parseUrl(req.url || '').pathname!

    // Complete the response with cached data if its present
    const cachedData = await getSprCache(sprCacheKey)
    if (cachedData) {
      const data = isSprData
        ? JSON.stringify(cachedData.pageData)
        : cachedData.html

      this.__sendPayload(
        res,
        data,
J
JJ Kasper 已提交
722 723
        isSprData ? 'application/json' : 'text/html; charset=utf-8',
        cachedData.curRevalidate
J
JJ Kasper 已提交
724 725 726 727 728 729
      )

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

J
JJ Kasper 已提交
732 733 734 735 736
    // 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 已提交
737 738 739
      let { pathname } = parseUrl(req.url || '', true)
      pathname = !pathname || pathname === '/' ? '/index' : pathname
      req.url = `/_next/data/${this.buildId}${pathname}.json`
J
JJ Kasper 已提交
740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771
    }

    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 }
772
    })
J
JJ Kasper 已提交
773 774 775 776 777 778 779 780

    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 已提交
781 782
            isSprData ? 'application/json' : 'text/html; charset=utf-8',
            sprRevalidate
J
JJ Kasper 已提交
783 784 785 786 787 788 789 790 791 792 793 794 795 796 797
          )
        }

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

        return null
      }
    )
798 799
  }

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

          return Promise.reject(err)
864
        }
J
Joe Haddad 已提交
865
      )
866
      .catch(err => {
J
Joe Haddad 已提交
867 868 869 870 871 872 873 874 875
        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 已提交
876 877
  }

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

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

J
Joe Haddad 已提交
925 926 927
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
928
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
929
  ): Promise<void> {
930 931
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
932
    if (!pathname) {
933 934
      throw new Error('pathname is undefined')
    }
N
Naoyuki Kanezawa 已提交
935
    res.statusCode = 404
936
    return this.renderError(null, req, res, pathname, query)
N
Naoyuki Kanezawa 已提交
937
  }
N
Naoyuki Kanezawa 已提交
938

J
Joe Haddad 已提交
939 940 941 942
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
943
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
944
  ): Promise<void> {
A
Arunoda Susiripala 已提交
945
    if (!this.isServeableUrl(path)) {
946
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
947 948
    }

949 950 951 952 953 954
    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 已提交
955
    try {
956
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
957
    } catch (err) {
T
Tim Neutkens 已提交
958
      if (err.code === 'ENOENT' || err.statusCode === 404) {
959
        this.render404(req, res, parsedUrl)
960 961 962
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
963 964 965 966 967 968
      } else {
        throw err
      }
    }
  }

969
  private isServeableUrl(path: string): boolean {
A
Arunoda Susiripala 已提交
970 971
    const resolved = resolve(path)
    if (
972
      resolved.indexOf(join(this.distDir) + sep) !== 0 &&
973 974
      resolved.indexOf(join(this.dir, 'static') + sep) !== 0 &&
      resolved.indexOf(join(this.dir, 'public') + sep) !== 0
A
Arunoda Susiripala 已提交
975 976 977 978 979 980 981 982
    ) {
      // Seems like the user is trying to traverse the filesystem.
      return false
    }

    return true
  }

983
  protected readBuildId(): string {
984 985 986 987 988
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
989 990 991
        throw new Error(
          `Could not find a valid build in the '${
            this.distDir
992
          }' directory! Try building your app with 'next build' before starting the server.`
J
Joe Haddad 已提交
993
        )
994 995 996
      }

      throw err
997
    }
998
  }
999 1000 1001 1002

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