next-server.ts 30.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'
J
Joe Haddad 已提交
5
import { compile as compilePathToRegex } 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
  PAGES_MANIFEST,
J
Joe Haddad 已提交
16
  PHASE_PRODUCTION_SERVER,
17
  ROUTES_MANIFEST,
J
Joe Haddad 已提交
18
  SERVER_DIRECTORY,
19
  SERVERLESS_DIRECTORY,
20
  DEFAULT_REDIRECT_STATUS,
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'
42
import { Redirect, Rewrite } from '../../lib/check-custom-routes'
J
JJ Kasper 已提交
43 44

const getCustomRouteMatcher = pathMatch(true)
45 46 47

type NextConfig = any

48 49 50 51 52 53
type Middleware = (
  req: IncomingMessage,
  res: ServerResponse,
  next: (err?: Error) => void
) => void

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

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

J
Joe Haddad 已提交
104 105 106 107 108
  public constructor({
    dir = '.',
    staticMarkup = false,
    quiet = false,
    conf = null,
J
JJ Kasper 已提交
109
    dev = false,
J
Joe Haddad 已提交
110
  }: ServerConstructor = {}) {
N
nkzawa 已提交
111
    this.dir = resolve(dir)
N
Naoyuki Kanezawa 已提交
112
    this.quiet = quiet
T
Tim Neutkens 已提交
113
    const phase = this.currentPhase()
114
    this.nextConfig = loadConfig(phase, this.dir, conf)
115
    this.distDir = join(this.dir, this.nextConfig.distDir)
116
    this.publicDir = join(this.dir, CLIENT_PUBLIC_FILES_PATH)
117
    this.hasStaticDir = fs.existsSync(join(this.dir, 'static'))
T
Tim Neutkens 已提交
118

119 120
    // Only serverRuntimeConfig needs the default
    // publicRuntimeConfig gets it's default in client/index.js
J
Joe Haddad 已提交
121 122 123 124 125
    const {
      serverRuntimeConfig = {},
      publicRuntimeConfig,
      assetPrefix,
      generateEtags,
126
      compress,
J
Joe Haddad 已提交
127
    } = this.nextConfig
128

T
Tim Neutkens 已提交
129
    this.buildId = this.readBuildId()
130

131
    this.renderOpts = {
T
Tim Neutkens 已提交
132
      ampBindInitData: this.nextConfig.experimental.ampBindInitData,
T
Tim Neutkens 已提交
133
      poweredByHeader: this.nextConfig.poweredByHeader,
134
      canonicalBase: this.nextConfig.amp.canonicalBase,
135 136
      documentMiddlewareEnabled: this.nextConfig.experimental
        .documentMiddleware,
J
Joe Haddad 已提交
137
      hasCssMode: this.nextConfig.experimental.css,
138
      staticMarkup,
139
      buildId: this.buildId,
140
      generateEtags,
141
    }
N
Naoyuki Kanezawa 已提交
142

143 144
    // Only the `publicRuntimeConfig` key is exposed to the client side
    // It'll be rendered as part of __NEXT_DATA__ on the client side
145
    if (Object.keys(publicRuntimeConfig).length > 0) {
146
      this.renderOpts.runtimeConfig = publicRuntimeConfig
147 148
    }

149
    if (compress && this.nextConfig.target === 'server') {
150 151 152
      this.compression = compression() as Middleware
    }

153
    // Initialize next/config with the environment configuration
154 155 156 157 158 159
    if (this.nextConfig.target === 'server') {
      envConfig.setConfig({
        serverRuntimeConfig,
        publicRuntimeConfig,
      })
    }
160

161 162 163 164 165 166 167 168 169 170
    this.serverBuildDir = join(
      this.distDir,
      this._isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY
    )
    const pagesManifestPath = join(this.serverBuildDir, PAGES_MANIFEST)

    if (!dev) {
      this.pagesManifest = require(pagesManifestPath)
    }

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

174 175 176
    // call init-server middleware, this is also handled
    // individually in serverless bundles when deployed
    if (!dev && this.nextConfig.experimental.plugins) {
177 178
      const initServer = require(join(this.serverBuildDir, 'init-server.js'))
        .default
179
      this.onErrorMiddleware = require(join(
180
        this.serverBuildDir,
181 182 183 184 185
        'on-error-server.js'
      )).default
      initServer()
    }

J
JJ Kasper 已提交
186 187 188 189 190 191 192 193 194 195 196 197
    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 已提交
198
  }
N
nkzawa 已提交
199

200
  protected currentPhase(): string {
201
    return PHASE_PRODUCTION_SERVER
202 203
  }

204 205 206 207
  private logError(err: Error): void {
    if (this.onErrorMiddleware) {
      this.onErrorMiddleware({ err })
    }
208 209
    if (this.quiet) return
    // tslint:disable-next-line
210
    console.error(err)
211 212
  }

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

224 225 226
    // 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 已提交
227
    }
228

229
    res.statusCode = 200
230
    return this.run(req, res, parsedUrl).catch(err => {
J
Joe Haddad 已提交
231 232 233 234
      this.logError(err)
      res.statusCode = 500
      res.end('Internal Server Error')
    })
235 236
  }

237
  public getRequestHandler() {
238
    return this.handleRequest.bind(this)
N
nkzawa 已提交
239 240
  }

241
  public setAssetPrefix(prefix?: string) {
242
    this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
243 244
  }

245
  // Backwards compatibility
246
  public async prepare(): Promise<void> {}
N
nkzawa 已提交
247

T
Tim Neutkens 已提交
248
  // Backwards compatibility
249
  protected async close(): Promise<void> {}
T
Tim Neutkens 已提交
250

251
  protected setImmutableAssetCacheControl(res: ServerResponse) {
T
Tim Neutkens 已提交
252
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
N
nkzawa 已提交
253 254
  }

J
JJ Kasper 已提交
255 256 257 258
  protected getCustomRoutes() {
    return require(join(this.distDir, ROUTES_MANIFEST))
  }

259
  protected generateRoutes(): Route[] {
J
JJ Kasper 已提交
260 261
    this.customRoutes = this.getCustomRoutes()

262 263 264
    const publicRoutes = fs.existsSync(this.publicDir)
      ? this.generatePublicRoutes()
      : []
J
JJ Kasper 已提交
265

266
    const staticFilesRoute = this.hasStaticDir
267 268 269 270 271 272 273
      ? [
          {
            // 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*'),
274
            name: 'static catchall',
275 276 277
            fn: async (req, res, params, parsedUrl) => {
              const p = join(this.dir, 'static', ...(params.path || []))
              await this.serveStatic(req, res, p, parsedUrl)
278 279 280
              return {
                finished: true,
              }
281 282 283 284
            },
          } as Route,
        ]
      : []
285

286
    const topRoutes: Route[] = [
T
Tim Neutkens 已提交
287
      {
288
        match: route('/_next/static/:path*'),
289 290
        type: 'route',
        name: '_next/static catchall',
291
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
292 293 294
          // The commons folder holds commonschunk files
          // The chunks folder holds dynamic entries
          // The buildId folder holds pages and potentially other assets. As buildId changes per build it can be long-term cached.
295 296

          // make sure to 404 for /_next/static itself
297 298 299 300 301 302
          if (!params.path) {
            await this.render404(req, res, parsedUrl)
            return {
              finished: true,
            }
          }
303

J
Joe Haddad 已提交
304 305 306 307 308
          if (
            params.path[0] === CLIENT_STATIC_FILES_RUNTIME ||
            params.path[0] === 'chunks' ||
            params.path[0] === this.buildId
          ) {
T
Tim Neutkens 已提交
309
            this.setImmutableAssetCacheControl(res)
310
          }
J
Joe Haddad 已提交
311 312 313
          const p = join(
            this.distDir,
            CLIENT_STATIC_FILES_PATH,
314
            ...(params.path || [])
J
Joe Haddad 已提交
315
          )
316
          await this.serveStatic(req, res, p, parsedUrl)
317 318 319
          return {
            finished: true,
          }
320
        },
321
      },
J
JJ Kasper 已提交
322 323
      {
        match: route('/_next/data/:path*'),
324 325
        type: 'route',
        name: '_next/data catchall',
J
JJ Kasper 已提交
326
        fn: async (req, res, params, _parsedUrl) => {
J
JJ Kasper 已提交
327 328 329
          // 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) {
330 331 332 333
            await this.render404(req, res, _parsedUrl)
            return {
              finished: true,
            }
J
JJ Kasper 已提交
334 335 336 337 338 339
          }
          // 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')) {
340 341 342 343
            await this.render404(req, res, _parsedUrl)
            return {
              finished: true,
            }
J
JJ Kasper 已提交
344 345 346 347 348 349 350
          }

          // re-create page's pathname
          const pathname = `/${params.path.join('/')}`
            .replace(/\.json$/, '')
            .replace(/\/index$/, '/')

J
JJ Kasper 已提交
351 352 353 354 355 356 357 358 359
          req.url = pathname
          const parsedUrl = parseUrl(pathname, true)
          await this.render(
            req,
            res,
            pathname,
            { _nextSprData: '1' },
            parsedUrl
          )
360 361 362
          return {
            finished: true,
          }
J
JJ Kasper 已提交
363 364
        },
      },
T
Tim Neutkens 已提交
365
      {
366
        match: route('/_next/:path*'),
367 368
        type: 'route',
        name: '_next catchall',
T
Tim Neutkens 已提交
369
        // This path is needed because `render()` does a check for `/_next` and the calls the routing again
370
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
371
          await this.render404(req, res, parsedUrl)
372 373 374
          return {
            finished: true,
          }
L
Lukáš Huvar 已提交
375 376
        },
      },
377 378
      ...publicRoutes,
      ...staticFilesRoute,
T
Tim Neutkens 已提交
379
    ]
380
    const routes: Route[] = [...topRoutes]
381

J
JJ Kasper 已提交
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
    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(
400
        ...customRoutes.map(route => {
J
JJ Kasper 已提交
401
          return {
402 403 404 405 406
            match: route.matcher,
            type: route.type,
            statusCode: route.statusCode,
            name: `${route.type} ${route.source} route`,
            fn: async (_req, res, params, _parsedUrl) => {
407 408 409 410
              const parsedDestination = parseUrl(route.destination, true)
              let destinationCompiler = compilePathToRegex(
                `${parsedDestination.pathname!}${parsedDestination.hash || ''}`
              )
411 412 413 414 415 416
              let newUrl

              try {
                newUrl = destinationCompiler(params)
              } catch (err) {
                if (
J
Joe Haddad 已提交
417 418 419
                  err.message.match(
                    /Expected .*? to not repeat, but got an array/
                  )
420 421 422 423 424 425 426
                ) {
                  throw new Error(
                    `To use a multi-match in the destination you must add \`*\` at the end of the param name to signify it should repeat. https://err.sh/zeit/next.js/invalid-multi-match`
                  )
                }
                throw err
              }
427 428

              if (route.type === 'redirect') {
429 430 431 432 433 434 435 436 437
                const parsedNewUrl = parseUrl(newUrl)
                res.setHeader(
                  'Location',
                  formatUrl({
                    ...parsedDestination,
                    pathname: parsedNewUrl.pathname,
                    hash: parsedNewUrl.hash,
                  })
                )
438 439 440 441
                res.statusCode = route.statusCode || DEFAULT_REDIRECT_STATUS
                res.end()
                return {
                  finished: true,
J
JJ Kasper 已提交
442 443 444
                }
              }

445 446 447
              return {
                finished: false,
                pathname: newUrl,
J
JJ Kasper 已提交
448 449 450 451 452
              }
            },
          } as Route
        })
      )
453 454 455
      // make sure previous routes can still be rewritten to by
      // custom routes e.g. /docs/_next/static -> /_next/static
      routes.push(...topRoutes)
J
JJ Kasper 已提交
456 457
    }

458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473
    routes.push({
      match: route('/api/:path*'),
      type: 'route',
      name: 'API Route',
      fn: async (req, res, params, parsedUrl) => {
        const { pathname } = parsedUrl
        await this.handleApiRequest(
          req as NextApiRequest,
          res as NextApiResponse,
          pathname!
        )
        return {
          finished: true,
        }
      },
    })
474

475
    if (this.nextConfig.useFileSystemPublicRoutes) {
J
Joe Haddad 已提交
476
      this.dynamicRoutes = this.getDynamicRoutes()
J
Joe Haddad 已提交
477

478 479 480
      // 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.
481
      // See more: https://github.com/zeit/next.js/issues/2617
T
Tim Neutkens 已提交
482
      routes.push({
483
        match: route('/:path*'),
484 485 486
        type: 'route',
        name: 'Catchall render',
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
487
          const { pathname, query } = parsedUrl
488
          if (!pathname) {
489 490
            throw new Error('pathname is undefined')
          }
491

492 493 494 495 496 497 498
          // Used in development to check public directory paths
          if (await this._beforeCatchAllRender(req, res, params, parsedUrl)) {
            return {
              finished: true,
            }
          }

499
          await this.render(req, res, pathname, query, parsedUrl)
500 501 502
          return {
            finished: true,
          }
503
        },
T
Tim Neutkens 已提交
504
      })
505
    }
N
nkzawa 已提交
506

T
Tim Neutkens 已提交
507 508 509
    return routes
  }

510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527
  private async getPagePath(pathname: string) {
    return getPagePath(
      pathname,
      this.distDir,
      this._isLikeServerless,
      this.renderOpts.dev
    )
  }

  protected async hasPage(pathname: string): Promise<boolean> {
    let found = false
    try {
      found = !!(await this.getPagePath(pathname))
    } catch (_) {}

    return found
  }

528 529 530 531 532 533 534 535 536
  protected async _beforeCatchAllRender(
    _req: IncomingMessage,
    _res: ServerResponse,
    _params: Params,
    _parsedUrl: UrlWithParsedQuery
  ) {
    return false
  }

537 538 539
  // Used to build API page in development
  protected async ensureApiPage(pathname: string) {}

L
Lukáš Huvar 已提交
540 541 542 543 544 545
  /**
   * Resolves `API` request, in development builds on demand
   * @param req http request
   * @param res http response
   * @param pathname path of request
   */
J
Joe Haddad 已提交
546
  private async handleApiRequest(
547 548
    req: IncomingMessage,
    res: ServerResponse,
549
    pathname: string
J
Joe Haddad 已提交
550
  ) {
551
    let page = pathname
L
Lukáš Huvar 已提交
552
    let params: Params | boolean = false
553
    let pageFound = await this.hasPage(page)
J
JJ Kasper 已提交
554

555
    if (!pageFound && this.dynamicRoutes) {
L
Lukáš Huvar 已提交
556 557 558
      for (const dynamicRoute of this.dynamicRoutes) {
        params = dynamicRoute.match(pathname)
        if (params) {
559 560
          page = dynamicRoute.page
          pageFound = true
L
Lukáš Huvar 已提交
561 562 563 564 565
          break
        }
      }
    }

566
    if (!pageFound) {
J
JJ Kasper 已提交
567 568
      return this.render404(req, res)
    }
569 570 571 572 573 574
    // Make sure the page is built before getting the path
    // or else it won't be in the manifest yet
    await this.ensureApiPage(page)

    const builtPagePath = await this.getPagePath(page)
    const pageModule = require(builtPagePath)
J
JJ Kasper 已提交
575

576
    if (!this.renderOpts.dev && this._isLikeServerless) {
577 578
      if (typeof pageModule.default === 'function') {
        return pageModule.default(req, res)
J
JJ Kasper 已提交
579 580 581
      }
    }

582
    await apiResolver(req, res, params, pageModule, this.onErrorMiddleware)
L
Lukáš Huvar 已提交
583 584
  }

585
  protected generatePublicRoutes(): Route[] {
586 587 588
    const routes: Route[] = []
    const publicFiles = recursiveReadDirSync(this.publicDir)

589
    publicFiles.forEach(path => {
590 591
      const unixPath = path.replace(/\\/g, '/')
      // Only include public files that will not replace a page path
592 593
      // this should not occur now that we check this during build
      if (!this.pagesManifest![unixPath]) {
594 595
        routes.push({
          match: route(unixPath),
596 597
          type: 'route',
          name: 'public catchall',
598 599 600
          fn: async (req, res, _params, parsedUrl) => {
            const p = join(this.publicDir, unixPath)
            await this.serveStatic(req, res, p, parsedUrl)
601 602 603
            return {
              finished: true,
            }
604 605 606 607 608 609 610 611
          },
        })
      }
    })

    return routes
  }

612
  protected getDynamicRoutes() {
613 614 615
    const dynamicRoutedPages = Object.keys(this.pagesManifest!).filter(
      isDynamicRoute
    )
616 617 618 619
    return getSortedRoutes(dynamicRoutedPages).map(page => ({
      page,
      match: getRouteMatcher(getRouteRegex(page)),
    }))
J
Joe Haddad 已提交
620 621
  }

622 623 624 625 626 627
  private handleCompression(req: IncomingMessage, res: ServerResponse) {
    if (this.compression) {
      this.compression(req, res, () => {})
    }
  }

628
  protected async run(
J
Joe Haddad 已提交
629 630
    req: IncomingMessage,
    res: ServerResponse,
631
    parsedUrl: UrlWithParsedQuery
J
Joe Haddad 已提交
632
  ) {
633 634
    this.handleCompression(req, res)

635
    try {
636 637
      const matched = await this.router.execute(req, res, parsedUrl)
      if (matched) {
638 639 640 641 642 643 644 645
        return
      }
    } catch (err) {
      if (err.code === 'DECODE_FAILED') {
        res.statusCode = 400
        return this.renderError(null, req, res, '/_error', {})
      }
      throw err
646 647
    }

648
    await this.render404(req, res, parsedUrl)
N
nkzawa 已提交
649 650
  }

651
  protected async sendHTML(
J
Joe Haddad 已提交
652 653
    req: IncomingMessage,
    res: ServerResponse,
654
    html: string
J
Joe Haddad 已提交
655
  ) {
T
Tim Neutkens 已提交
656 657
    const { generateEtags, poweredByHeader } = this.renderOpts
    return sendHTML(req, res, html, { generateEtags, poweredByHeader })
658 659
  }

J
Joe Haddad 已提交
660 661 662 663 664
  public async render(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
665
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
666
  ): Promise<void> {
667
    const url: any = req.url
668 669 670 671 672

    if (
      url.match(/^\/_next\//) ||
      (this.hasStaticDir && url.match(/^\/static\//))
    ) {
673 674 675
      return this.handleRequest(req, res, parsedUrl)
    }

676
    if (isBlockedPage(pathname)) {
677
      return this.render404(req, res, parsedUrl)
678 679
    }

680
    const html = await this.renderToHTML(req, res, pathname, query, {
J
Joe Haddad 已提交
681 682 683 684 685
      dataOnly:
        (this.renderOpts.ampBindInitData && Boolean(query.dataOnly)) ||
        (req.headers &&
          (req.headers.accept || '').indexOf('application/amp.bind+json') !==
            -1),
686
    })
687 688
    // Request was ended by the user
    if (html === null) {
689 690 691
      return
    }

692
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
693
  }
N
nkzawa 已提交
694

J
Joe Haddad 已提交
695
  private async findPageComponents(
J
Joe Haddad 已提交
696
    pathname: string,
697
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
698
  ) {
699
    const serverless = !this.renderOpts.dev && this._isLikeServerless
J
JJ Kasper 已提交
700 701 702
    // try serving a static AMP version first
    if (query.amp) {
      try {
J
Joe Haddad 已提交
703 704 705 706
        return await loadComponents(
          this.distDir,
          this.buildId,
          (pathname === '/' ? '/index' : pathname) + '.amp',
707
          serverless
J
Joe Haddad 已提交
708
        )
J
JJ Kasper 已提交
709 710 711 712
      } catch (err) {
        if (err.code !== 'ENOENT') throw err
      }
    }
J
Joe Haddad 已提交
713 714 715 716
    return await loadComponents(
      this.distDir,
      this.buildId,
      pathname,
717
      serverless
J
Joe Haddad 已提交
718 719 720
    )
  }

J
JJ Kasper 已提交
721 722 723 724 725 726
  private __sendPayload(
    res: ServerResponse,
    payload: any,
    type: string,
    revalidate?: number | false
  ) {
J
JJ Kasper 已提交
727
    // TODO: ETag? Cache-Control headers? Next-specific headers?
J
JJ Kasper 已提交
728
    res.setHeader('Content-Type', type)
J
JJ Kasper 已提交
729
    res.setHeader('Content-Length', Buffer.byteLength(payload))
J
Joe Haddad 已提交
730 731 732 733 734 735 736 737 738 739 740 741
    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 已提交
742
    }
J
JJ Kasper 已提交
743 744 745
    res.end(payload)
  }

J
Joe Haddad 已提交
746 747 748 749 750 751
  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
    result: LoadComponentsReturnType,
752
    opts: any
J
JJ Kasper 已提交
753
  ): Promise<string | null> {
J
JJ Kasper 已提交
754
    // handle static page
J
Joe Haddad 已提交
755 756 757 758
    if (typeof result.Component === 'string') {
      return result.Component
    }

J
JJ Kasper 已提交
759 760
    // check request state
    const isLikeServerless =
J
Joe Haddad 已提交
761
      typeof result.Component === 'object' &&
J
JJ Kasper 已提交
762
      typeof result.Component.renderReqToHTML === 'function'
J
JJ Kasper 已提交
763 764 765 766 767 768
    const isSpr = !!result.unstable_getStaticProps

    // non-spr requests should render like normal
    if (!isSpr) {
      // handle serverless
      if (isLikeServerless) {
769 770 771 772 773 774 775 776
        const curUrl = parseUrl(req.url!, true)
        req.url = formatUrl({
          ...curUrl,
          query: {
            ...curUrl.query,
            ...query,
          },
        })
J
JJ Kasper 已提交
777 778 779 780 781 782 783 784 785 786 787
        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
788 789
    delete query._nextSprData

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

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

J
JJ Kasper 已提交
813 814 815 816 817
    // 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 已提交
818 819 820
      let { pathname } = parseUrl(req.url || '', true)
      pathname = !pathname || pathname === '/' ? '/index' : pathname
      req.url = `/_next/data/${this.buildId}${pathname}.json`
J
JJ Kasper 已提交
821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852
    }

    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 }
853
    })
J
JJ Kasper 已提交
854 855 856 857 858 859 860 861

    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 已提交
862 863
            isSprData ? 'application/json' : 'text/html; charset=utf-8',
            sprRevalidate
J
JJ Kasper 已提交
864 865 866 867 868 869 870 871 872 873 874 875 876 877 878
          )
        }

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

        return null
      }
    )
879 880
  }

J
Joe Haddad 已提交
881
  public renderToHTML(
J
Joe Haddad 已提交
882 883 884 885
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
J
Joe Haddad 已提交
886 887 888 889 890 891 892
    {
      amphtml,
      dataOnly,
      hasAmp,
    }: {
      amphtml?: boolean
      hasAmp?: boolean
893 894
      dataOnly?: boolean
    } = {}
J
Joe Haddad 已提交
895
  ): Promise<string | null> {
J
Joe Haddad 已提交
896 897
    return this.findPageComponents(pathname, query)
      .then(
898
        result => {
J
Joe Haddad 已提交
899 900 901 902
          return this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
903 904 905
            result.unstable_getStaticProps
              ? { _nextSprData: query._nextSprData }
              : query,
J
Joe Haddad 已提交
906
            result,
907
            { ...this.renderOpts, amphtml, hasAmp, dataOnly }
J
Joe Haddad 已提交
908 909
          )
        },
910
        err => {
J
Joe Haddad 已提交
911 912 913 914 915 916 917 918 919 920 921
          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(
922 923
              result => {
                return this.renderToHTMLWithComponents(
J
Joe Haddad 已提交
924 925 926
                  req,
                  res,
                  dynamicRoute.page,
J
JJ Kasper 已提交
927 928 929 930 931 932 933
                  // only add params for SPR enabled pages
                  {
                    ...(result.unstable_getStaticProps
                      ? { _nextSprData: query._nextSprData }
                      : query),
                    ...params,
                  },
J
Joe Haddad 已提交
934
                  result,
J
JJ Kasper 已提交
935 936 937 938 939 940
                  {
                    ...this.renderOpts,
                    amphtml,
                    hasAmp,
                    dataOnly,
                  }
941
                )
942
              }
J
Joe Haddad 已提交
943 944 945 946
            )
          }

          return Promise.reject(err)
947
        }
J
Joe Haddad 已提交
948
      )
949
      .catch(err => {
J
Joe Haddad 已提交
950 951 952 953 954 955 956 957 958
        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 已提交
959 960
  }

J
Joe Haddad 已提交
961 962 963 964 965
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
966
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
967 968 969
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
970
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
971
    )
N
Naoyuki Kanezawa 已提交
972
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
973
    if (html === null) {
974 975
      return
    }
976
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
977 978
  }

J
Joe Haddad 已提交
979 980 981 982 983
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
984
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
985
  ) {
J
Joe Haddad 已提交
986
    const result = await this.findPageComponents('/_error', query)
987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005
    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 已提交
1006 1007
  }

J
Joe Haddad 已提交
1008 1009 1010
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
1011
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1012
  ): Promise<void> {
1013 1014
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
1015
    if (!pathname) {
1016 1017
      throw new Error('pathname is undefined')
    }
N
Naoyuki Kanezawa 已提交
1018
    res.statusCode = 404
1019
    return this.renderError(null, req, res, pathname, query)
N
Naoyuki Kanezawa 已提交
1020
  }
N
Naoyuki Kanezawa 已提交
1021

J
Joe Haddad 已提交
1022 1023 1024 1025
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
1026
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1027
  ): Promise<void> {
A
Arunoda Susiripala 已提交
1028
    if (!this.isServeableUrl(path)) {
1029
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
1030 1031
    }

1032 1033 1034 1035 1036 1037
    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 已提交
1038
    try {
1039
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
1040
    } catch (err) {
T
Tim Neutkens 已提交
1041
      if (err.code === 'ENOENT' || err.statusCode === 404) {
1042
        this.render404(req, res, parsedUrl)
1043 1044 1045
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
1046 1047 1048 1049 1050 1051
      } else {
        throw err
      }
    }
  }

1052
  private isServeableUrl(path: string): boolean {
A
Arunoda Susiripala 已提交
1053 1054
    const resolved = resolve(path)
    if (
1055
      resolved.indexOf(join(this.distDir) + sep) !== 0 &&
1056 1057
      resolved.indexOf(join(this.dir, 'static') + sep) !== 0 &&
      resolved.indexOf(join(this.dir, 'public') + sep) !== 0
A
Arunoda Susiripala 已提交
1058 1059 1060 1061 1062 1063 1064 1065
    ) {
      // Seems like the user is trying to traverse the filesystem.
      return false
    }

    return true
  }

1066
  protected readBuildId(): string {
1067 1068 1069 1070 1071
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
1072
        throw new Error(
1073
          `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 已提交
1074
        )
1075 1076 1077
      }

      throw err
1078
    }
1079
  }
1080 1081 1082 1083

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