next-server.ts 33.7 KB
Newer Older
1
import compression from 'compression'
J
Joe Haddad 已提交
2
import fs from 'fs'
3
import Proxy from 'http-proxy'
J
Joe Haddad 已提交
4
import { IncomingMessage, ServerResponse } from 'http'
J
Joe Haddad 已提交
5
import { join, resolve, sep } from 'path'
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,
T
Tim Neutkens 已提交
20
} from '../lib/constants'
J
Joe Haddad 已提交
21 22 23 24
import {
  getRouteMatcher,
  getRouteRegex,
  getSortedRoutes,
25
  isDynamicRoute,
J
Joe Haddad 已提交
26
} from '../lib/router/utils'
27
import * as envConfig from '../lib/runtime-config'
J
Joe Haddad 已提交
28
import { isResSent, NextApiRequest, NextApiResponse } from '../lib/utils'
29
import { apiResolver } from './api-utils'
30
import loadConfig, { isTargetLikeServerless } from './config'
31
import pathMatch from './lib/path-match'
J
Joe Haddad 已提交
32
import { recursiveReadDirSync } from './lib/recursive-readdir-sync'
33
import { loadComponents, LoadComponentsReturnType } from './load-components'
J
Joe Haddad 已提交
34
import { renderToHTML } from './render'
J
Joe Haddad 已提交
35
import { getPagePath } from './require'
36 37 38 39 40 41
import Router, {
  Params,
  route,
  Route,
  DynamicRoutes,
  PageChecker,
42
  prepareDestination,
43
} from './router'
J
Joe Haddad 已提交
44 45
import { sendHTML } from './send-html'
import { serveStatic } from './serve-static'
46 47 48 49 50 51
import {
  getSprCache,
  initializeSprCache,
  setSprCache,
  getFallback,
} from './spr-cache'
52
import { isBlockedPage } from './utils'
53 54 55 56 57
import {
  Redirect,
  Rewrite,
  RouteType,
  Header,
58
  getRedirectStatus,
59
} from '../../lib/check-custom-routes'
60
import { normalizePagePath } from './normalize-page-path'
J
JJ Kasper 已提交
61 62

const getCustomRouteMatcher = pathMatch(true)
63 64 65

type NextConfig = any

66 67 68 69 70 71
type Middleware = (
  req: IncomingMessage,
  res: ServerResponse,
  next: (err?: Error) => void
) => void

T
Tim Neutkens 已提交
72
export type ServerConstructor = {
73 74 75
  /**
   * Where the Next project is located - @default '.'
   */
J
Joe Haddad 已提交
76 77
  dir?: string
  staticMarkup?: boolean
78 79 80
  /**
   * Hide error messages containing server information - @default false
   */
J
Joe Haddad 已提交
81
  quiet?: boolean
82 83 84
  /**
   * Object what you would use in next.config.js - @default {}
   */
85
  conf?: NextConfig
J
JJ Kasper 已提交
86
  dev?: boolean
87
}
88

N
nkzawa 已提交
89
export default class Server {
90 91 92 93
  dir: string
  quiet: boolean
  nextConfig: NextConfig
  distDir: string
94
  pagesDir?: string
95
  publicDir: string
96
  hasStaticDir: boolean
97 98
  serverBuildDir: string
  pagesManifest?: { [name: string]: string }
99 100
  buildId: string
  renderOpts: {
T
Tim Neutkens 已提交
101
    poweredByHeader: boolean
J
Joe Haddad 已提交
102 103 104 105
    staticMarkup: boolean
    buildId: string
    generateEtags: boolean
    runtimeConfig?: { [key: string]: any }
106 107
    assetPrefix?: string
    canonicalBase: string
108
    documentMiddlewareEnabled: boolean
J
Joe Haddad 已提交
109
    hasCssMode: boolean
110
    dev?: boolean
111
    pages404?: boolean
112
  }
113
  private compression?: Middleware
J
JJ Kasper 已提交
114
  private onErrorMiddleware?: ({ err }: { err: Error }) => Promise<void>
115
  router: Router
116
  protected dynamicRoutes?: DynamicRoutes
J
JJ Kasper 已提交
117 118 119
  protected customRoutes?: {
    rewrites: Rewrite[]
    redirects: Redirect[]
120
    headers: Header[]
J
JJ Kasper 已提交
121
  }
122

J
Joe Haddad 已提交
123 124 125 126 127
  public constructor({
    dir = '.',
    staticMarkup = false,
    quiet = false,
    conf = null,
J
JJ Kasper 已提交
128
    dev = false,
J
Joe Haddad 已提交
129
  }: ServerConstructor = {}) {
N
nkzawa 已提交
130
    this.dir = resolve(dir)
N
Naoyuki Kanezawa 已提交
131
    this.quiet = quiet
T
Tim Neutkens 已提交
132
    const phase = this.currentPhase()
133
    this.nextConfig = loadConfig(phase, this.dir, conf)
134
    this.distDir = join(this.dir, this.nextConfig.distDir)
135
    this.publicDir = join(this.dir, CLIENT_PUBLIC_FILES_PATH)
136
    this.hasStaticDir = fs.existsSync(join(this.dir, 'static'))
T
Tim Neutkens 已提交
137

138 139
    // Only serverRuntimeConfig needs the default
    // publicRuntimeConfig gets it's default in client/index.js
J
Joe Haddad 已提交
140 141 142 143 144
    const {
      serverRuntimeConfig = {},
      publicRuntimeConfig,
      assetPrefix,
      generateEtags,
145
      compress,
J
Joe Haddad 已提交
146
    } = this.nextConfig
147

T
Tim Neutkens 已提交
148
    this.buildId = this.readBuildId()
149

150
    this.renderOpts = {
T
Tim Neutkens 已提交
151
      poweredByHeader: this.nextConfig.poweredByHeader,
152
      canonicalBase: this.nextConfig.amp.canonicalBase,
153 154
      documentMiddlewareEnabled: this.nextConfig.experimental
        .documentMiddleware,
J
Joe Haddad 已提交
155
      hasCssMode: this.nextConfig.experimental.css,
156
      staticMarkup,
157
      buildId: this.buildId,
158
      generateEtags,
159
      pages404: this.nextConfig.experimental.pages404,
160
    }
N
Naoyuki Kanezawa 已提交
161

162 163
    // Only the `publicRuntimeConfig` key is exposed to the client side
    // It'll be rendered as part of __NEXT_DATA__ on the client side
164
    if (Object.keys(publicRuntimeConfig).length > 0) {
165
      this.renderOpts.runtimeConfig = publicRuntimeConfig
166 167
    }

168
    if (compress && this.nextConfig.target === 'server') {
169 170 171
      this.compression = compression() as Middleware
    }

172
    // Initialize next/config with the environment configuration
173 174 175 176
    envConfig.setConfig({
      serverRuntimeConfig,
      publicRuntimeConfig,
    })
177

178 179 180 181 182 183 184 185 186 187
    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 已提交
188
    this.router = new Router(this.generateRoutes())
189
    this.setAssetPrefix(assetPrefix)
J
JJ Kasper 已提交
190

191 192 193
    // call init-server middleware, this is also handled
    // individually in serverless bundles when deployed
    if (!dev && this.nextConfig.experimental.plugins) {
194 195
      const initServer = require(join(this.serverBuildDir, 'init-server.js'))
        .default
196
      this.onErrorMiddleware = require(join(
197
        this.serverBuildDir,
198 199 200 201 202
        'on-error-server.js'
      )).default
      initServer()
    }

J
JJ Kasper 已提交
203 204 205 206 207 208 209 210 211 212 213 214
    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 已提交
215
  }
N
nkzawa 已提交
216

217
  protected currentPhase(): string {
218
    return PHASE_PRODUCTION_SERVER
219 220
  }

221 222 223 224
  private logError(err: Error): void {
    if (this.onErrorMiddleware) {
      this.onErrorMiddleware({ err })
    }
225 226
    if (this.quiet) return
    // tslint:disable-next-line
227
    console.error(err)
228 229
  }

J
Joe Haddad 已提交
230 231 232
  private handleRequest(
    req: IncomingMessage,
    res: ServerResponse,
233
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
234
  ): Promise<void> {
235
    // Parse url if parsedUrl not provided
236
    if (!parsedUrl || typeof parsedUrl !== 'object') {
237 238
      const url: any = req.url
      parsedUrl = parseUrl(url, true)
239
    }
240

241 242 243
    // 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 已提交
244
    }
245

T
Tim Neutkens 已提交
246 247 248 249 250 251 252 253 254 255
    if (parsedUrl.pathname!.startsWith(this.nextConfig.experimental.basePath)) {
      // If replace ends up replacing the full url it'll be `undefined`, meaning we have to default it to `/`
      parsedUrl.pathname =
        parsedUrl.pathname!.replace(
          this.nextConfig.experimental.basePath,
          ''
        ) || '/'
      req.url = req.url!.replace(this.nextConfig.experimental.basePath, '')
    }

256
    res.statusCode = 200
257
    return this.run(req, res, parsedUrl).catch(err => {
J
Joe Haddad 已提交
258 259 260 261
      this.logError(err)
      res.statusCode = 500
      res.end('Internal Server Error')
    })
262 263
  }

264
  public getRequestHandler() {
265
    return this.handleRequest.bind(this)
N
nkzawa 已提交
266 267
  }

268
  public setAssetPrefix(prefix?: string) {
269
    this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
270 271
  }

272
  // Backwards compatibility
273
  public async prepare(): Promise<void> {}
N
nkzawa 已提交
274

T
Tim Neutkens 已提交
275
  // Backwards compatibility
276
  protected async close(): Promise<void> {}
T
Tim Neutkens 已提交
277

278
  protected setImmutableAssetCacheControl(res: ServerResponse) {
T
Tim Neutkens 已提交
279
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
N
nkzawa 已提交
280 281
  }

J
JJ Kasper 已提交
282 283 284 285
  protected getCustomRoutes() {
    return require(join(this.distDir, ROUTES_MANIFEST))
  }

286
  protected generateRoutes(): {
287 288
    headers: Route[]
    rewrites: Route[]
289
    fsRoutes: Route[]
290
    redirects: Route[]
291 292
    catchAllRoute: Route
    pageChecker: PageChecker
293
    useFileSystemPublicRoutes: boolean
294 295
    dynamicRoutes: DynamicRoutes | undefined
  } {
J
JJ Kasper 已提交
296 297
    this.customRoutes = this.getCustomRoutes()

298 299 300
    const publicRoutes = fs.existsSync(this.publicDir)
      ? this.generatePublicRoutes()
      : []
J
JJ Kasper 已提交
301

302
    const staticFilesRoute = this.hasStaticDir
303 304 305 306 307 308 309
      ? [
          {
            // 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*'),
310
            name: 'static catchall',
311 312 313
            fn: async (req, res, params, parsedUrl) => {
              const p = join(this.dir, 'static', ...(params.path || []))
              await this.serveStatic(req, res, p, parsedUrl)
314 315 316
              return {
                finished: true,
              }
317 318 319 320
            },
          } as Route,
        ]
      : []
321

322 323 324 325
    let headers: Route[] = []
    let rewrites: Route[] = []
    let redirects: Route[] = []

326
    const fsRoutes: Route[] = [
T
Tim Neutkens 已提交
327
      {
328
        match: route('/_next/static/:path*'),
329 330
        type: 'route',
        name: '_next/static catchall',
331
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
332 333 334
          // 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.
335 336

          // make sure to 404 for /_next/static itself
337 338 339 340 341 342
          if (!params.path) {
            await this.render404(req, res, parsedUrl)
            return {
              finished: true,
            }
          }
343

J
Joe Haddad 已提交
344 345 346
          if (
            params.path[0] === CLIENT_STATIC_FILES_RUNTIME ||
            params.path[0] === 'chunks' ||
347 348
            params.path[0] === 'css' ||
            params.path[0] === 'media' ||
J
Joe Haddad 已提交
349 350
            params.path[0] === this.buildId
          ) {
T
Tim Neutkens 已提交
351
            this.setImmutableAssetCacheControl(res)
352
          }
J
Joe Haddad 已提交
353 354 355
          const p = join(
            this.distDir,
            CLIENT_STATIC_FILES_PATH,
356
            ...(params.path || [])
J
Joe Haddad 已提交
357
          )
358
          await this.serveStatic(req, res, p, parsedUrl)
359 360 361
          return {
            finished: true,
          }
362
        },
363
      },
J
JJ Kasper 已提交
364 365
      {
        match: route('/_next/data/:path*'),
366 367
        type: 'route',
        name: '_next/data catchall',
J
JJ Kasper 已提交
368
        fn: async (req, res, params, _parsedUrl) => {
J
JJ Kasper 已提交
369 370 371
          // 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) {
372 373 374 375
            await this.render404(req, res, _parsedUrl)
            return {
              finished: true,
            }
J
JJ Kasper 已提交
376 377 378 379 380 381
          }
          // 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')) {
382 383 384 385
            await this.render404(req, res, _parsedUrl)
            return {
              finished: true,
            }
J
JJ Kasper 已提交
386 387 388 389 390 391 392
          }

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

J
JJ Kasper 已提交
393 394 395 396 397 398
          req.url = pathname
          const parsedUrl = parseUrl(pathname, true)
          await this.render(
            req,
            res,
            pathname,
399
            { ..._parsedUrl.query, _nextDataReq: '1' },
J
JJ Kasper 已提交
400 401
            parsedUrl
          )
402 403 404
          return {
            finished: true,
          }
J
JJ Kasper 已提交
405 406
        },
      },
T
Tim Neutkens 已提交
407
      {
408
        match: route('/_next/:path*'),
409 410
        type: 'route',
        name: '_next catchall',
T
Tim Neutkens 已提交
411
        // This path is needed because `render()` does a check for `/_next` and the calls the routing again
412
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
413
          await this.render404(req, res, parsedUrl)
414 415 416
          return {
            finished: true,
          }
L
Lukáš Huvar 已提交
417 418
        },
      },
419 420
      ...publicRoutes,
      ...staticFilesRoute,
T
Tim Neutkens 已提交
421
    ]
422

J
JJ Kasper 已提交
423 424
    if (this.customRoutes) {
      const getCustomRoute = (
425 426
        r: Rewrite | Redirect | Header,
        type: RouteType
427 428 429 430 431 432 433 434
      ) =>
        ({
          ...r,
          type,
          match: getCustomRouteMatcher(r.source),
          name: type,
          fn: async (req, res, params, parsedUrl) => ({ finished: false }),
        } as Route & Rewrite & Header)
J
JJ Kasper 已提交
435

436
      // Headers come very first
437 438 439 440 441 442 443 444 445 446 447 448 449 450
      headers = this.customRoutes.headers.map(r => {
        const route = getCustomRoute(r, 'header')
        return {
          match: route.match,
          type: route.type,
          name: `${route.type} ${route.source} header route`,
          fn: async (_req, res, _params, _parsedUrl) => {
            for (const header of (route as Header).headers) {
              res.setHeader(header.key, header.value)
            }
            return { finished: false }
          },
        } as Route
      })
J
JJ Kasper 已提交
451

452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473
      redirects = this.customRoutes.redirects.map(redirect => {
        const route = getCustomRoute(redirect, 'redirect')
        return {
          type: route.type,
          match: route.match,
          statusCode: route.statusCode,
          name: `Redirect route`,
          fn: async (_req, res, params, _parsedUrl) => {
            const { parsedDestination } = prepareDestination(
              route.destination,
              params
            )
            const updatedDestination = formatUrl(parsedDestination)

            res.setHeader('Location', updatedDestination)
            res.statusCode = getRedirectStatus(route as Redirect)

            // Since IE11 doesn't support the 308 header add backwards
            // compatibility using refresh header
            if (res.statusCode === 308) {
              res.setHeader('Refresh', `0;url=${updatedDestination}`)
            }
474

475 476 477 478 479 480 481
            res.end()
            return {
              finished: true,
            }
          },
        } as Route
      })
482

483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502
      rewrites = this.customRoutes.rewrites.map(rewrite => {
        const route = getCustomRoute(rewrite, 'rewrite')
        return {
          check: true,
          type: route.type,
          name: `Rewrite route`,
          match: route.match,
          fn: async (req, res, params, _parsedUrl) => {
            const { newUrl, parsedDestination } = prepareDestination(
              route.destination,
              params
            )

            // external rewrite, proxy it
            if (parsedDestination.protocol) {
              const target = formatUrl(parsedDestination)
              const proxy = new Proxy({
                target,
                changeOrigin: true,
                ignorePath: true,
503
              })
504
              proxy.web(req, res)
505

506 507 508
              proxy.on('error', (err: Error) => {
                console.error(`Error occurred proxying ${target}`, err)
              })
509
              return {
510
                finished: true,
J
JJ Kasper 已提交
511
              }
512 513
            }
            ;(req as any)._nextDidRewrite = true
514

515 516 517 518 519 520 521 522
            return {
              finished: false,
              pathname: newUrl,
              query: parsedDestination.query,
            }
          },
        } as Route
      })
523 524 525 526 527 528 529 530 531 532 533 534
    }

    const catchAllRoute: Route = {
      match: route('/:path*'),
      type: 'route',
      name: 'Catchall render',
      fn: async (req, res, params, parsedUrl) => {
        const { pathname, query } = parsedUrl
        if (!pathname) {
          throw new Error('pathname is undefined')
        }

535
        if (params?.path?.[0] === 'api') {
536 537 538
          const handled = await this.handleApiRequest(
            req as NextApiRequest,
            res as NextApiResponse,
539 540
            pathname!,
            query
541 542 543 544 545 546 547
          )
          if (handled) {
            return { finished: true }
          }
        }

        await this.render(req, res, pathname, query, parsedUrl)
548 549 550 551
        return {
          finished: true,
        }
      },
552
    }
553

554
    const { useFileSystemPublicRoutes } = this.nextConfig
J
Joe Haddad 已提交
555

556 557
    if (useFileSystemPublicRoutes) {
      this.dynamicRoutes = this.getDynamicRoutes()
558
    }
N
nkzawa 已提交
559

560
    return {
561
      headers,
562
      fsRoutes,
563 564
      rewrites,
      redirects,
565
      catchAllRoute,
566
      useFileSystemPublicRoutes,
567 568 569
      dynamicRoutes: this.dynamicRoutes,
      pageChecker: this.hasPage.bind(this),
    }
T
Tim Neutkens 已提交
570 571
  }

572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589
  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
  }

590 591 592 593 594 595 596 597 598
  protected async _beforeCatchAllRender(
    _req: IncomingMessage,
    _res: ServerResponse,
    _params: Params,
    _parsedUrl: UrlWithParsedQuery
  ) {
    return false
  }

599 600 601
  // Used to build API page in development
  protected async ensureApiPage(pathname: string) {}

L
Lukáš Huvar 已提交
602 603 604 605 606 607
  /**
   * Resolves `API` request, in development builds on demand
   * @param req http request
   * @param res http response
   * @param pathname path of request
   */
J
Joe Haddad 已提交
608
  private async handleApiRequest(
609 610
    req: IncomingMessage,
    res: ServerResponse,
611 612
    pathname: string,
    query: ParsedUrlQuery
J
Joe Haddad 已提交
613
  ) {
614
    let page = pathname
L
Lukáš Huvar 已提交
615
    let params: Params | boolean = false
616
    let pageFound = await this.hasPage(page)
J
JJ Kasper 已提交
617

618
    if (!pageFound && this.dynamicRoutes) {
L
Lukáš Huvar 已提交
619 620
      for (const dynamicRoute of this.dynamicRoutes) {
        params = dynamicRoute.match(pathname)
621
        if (dynamicRoute.page.startsWith('/api') && params) {
622 623
          page = dynamicRoute.page
          pageFound = true
L
Lukáš Huvar 已提交
624 625 626 627 628
          break
        }
      }
    }

629
    if (!pageFound) {
630
      return false
J
JJ Kasper 已提交
631
    }
632 633 634 635 636 637
    // 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)
638
    query = { ...query, ...params }
J
JJ Kasper 已提交
639

640
    if (!this.renderOpts.dev && this._isLikeServerless) {
641
      if (typeof pageModule.default === 'function') {
642
        this.prepareServerlessUrl(req, query)
643 644
        await pageModule.default(req, res)
        return true
J
JJ Kasper 已提交
645 646 647
      }
    }

648
    await apiResolver(req, res, query, pageModule, this.onErrorMiddleware)
649
    return true
L
Lukáš Huvar 已提交
650 651
  }

652
  protected generatePublicRoutes(): Route[] {
653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671
    const publicFiles = new Set(
      recursiveReadDirSync(this.publicDir).map(p => p.replace(/\\/g, '/'))
    )

    return [
      {
        match: route('/:path*'),
        name: 'public folder catchall',
        fn: async (req, res, params, parsedUrl) => {
          const path = `/${(params.path || []).join('/')}`

          if (publicFiles.has(path)) {
            await this.serveStatic(
              req,
              res,
              // we need to re-encode it since send decodes it
              join(this.dir, 'public', encodeURIComponent(path)),
              parsedUrl
            )
672 673 674
            return {
              finished: true,
            }
675 676 677 678 679 680 681
          }
          return {
            finished: false,
          }
        },
      } as Route,
    ]
682 683
  }

684
  protected getDynamicRoutes() {
685 686 687
    const dynamicRoutedPages = Object.keys(this.pagesManifest!).filter(
      isDynamicRoute
    )
688 689 690 691
    return getSortedRoutes(dynamicRoutedPages).map(page => ({
      page,
      match: getRouteMatcher(getRouteRegex(page)),
    }))
J
Joe Haddad 已提交
692 693
  }

694 695 696 697 698 699
  private handleCompression(req: IncomingMessage, res: ServerResponse) {
    if (this.compression) {
      this.compression(req, res, () => {})
    }
  }

700
  protected async run(
J
Joe Haddad 已提交
701 702
    req: IncomingMessage,
    res: ServerResponse,
703
    parsedUrl: UrlWithParsedQuery
J
Joe Haddad 已提交
704
  ) {
705 706
    this.handleCompression(req, res)

707
    try {
708 709
      const matched = await this.router.execute(req, res, parsedUrl)
      if (matched) {
710 711 712 713 714 715 716 717
        return
      }
    } catch (err) {
      if (err.code === 'DECODE_FAILED') {
        res.statusCode = 400
        return this.renderError(null, req, res, '/_error', {})
      }
      throw err
718 719
    }

720
    await this.render404(req, res, parsedUrl)
N
nkzawa 已提交
721 722
  }

723
  protected async sendHTML(
J
Joe Haddad 已提交
724 725
    req: IncomingMessage,
    res: ServerResponse,
726
    html: string
J
Joe Haddad 已提交
727
  ) {
T
Tim Neutkens 已提交
728 729
    const { generateEtags, poweredByHeader } = this.renderOpts
    return sendHTML(req, res, html, { generateEtags, poweredByHeader })
730 731
  }

J
Joe Haddad 已提交
732 733 734 735 736
  public async render(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
737
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
738
  ): Promise<void> {
739
    const url: any = req.url
740 741 742 743 744

    if (
      url.match(/^\/_next\//) ||
      (this.hasStaticDir && url.match(/^\/static\//))
    ) {
745 746 747
      return this.handleRequest(req, res, parsedUrl)
    }

748
    if (isBlockedPage(pathname)) {
749
      return this.render404(req, res, parsedUrl)
750 751
    }

T
Tim Neutkens 已提交
752
    const html = await this.renderToHTML(req, res, pathname, query, {})
753 754
    // Request was ended by the user
    if (html === null) {
755 756 757
      return
    }

758
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
759
  }
N
nkzawa 已提交
760

J
Joe Haddad 已提交
761
  private async findPageComponents(
J
Joe Haddad 已提交
762
    pathname: string,
763
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
764
  ) {
765
    const serverless = !this.renderOpts.dev && this._isLikeServerless
J
JJ Kasper 已提交
766 767 768
    // try serving a static AMP version first
    if (query.amp) {
      try {
J
Joe Haddad 已提交
769 770 771
        return await loadComponents(
          this.distDir,
          this.buildId,
772
          normalizePagePath(pathname) + '.amp',
773
          serverless
J
Joe Haddad 已提交
774
        )
J
JJ Kasper 已提交
775 776 777 778
      } catch (err) {
        if (err.code !== 'ENOENT') throw err
      }
    }
J
Joe Haddad 已提交
779 780 781 782
    return await loadComponents(
      this.distDir,
      this.buildId,
      pathname,
783
      serverless
J
Joe Haddad 已提交
784 785 786
    )
  }

J
JJ Kasper 已提交
787 788 789 790 791 792
  private __sendPayload(
    res: ServerResponse,
    payload: any,
    type: string,
    revalidate?: number | false
  ) {
J
JJ Kasper 已提交
793
    // TODO: ETag? Cache-Control headers? Next-specific headers?
J
JJ Kasper 已提交
794
    res.setHeader('Content-Type', type)
J
JJ Kasper 已提交
795
    res.setHeader('Content-Length', Buffer.byteLength(payload))
J
Joe Haddad 已提交
796 797 798 799
    if (!this.renderOpts.dev) {
      if (revalidate) {
        res.setHeader(
          'Cache-Control',
800 801 802
          revalidate < 0
            ? `no-cache, no-store, must-revalidate`
            : `s-maxage=${revalidate}, stale-while-revalidate`
J
Joe Haddad 已提交
803 804 805 806 807 808 809
        )
      } else if (revalidate === false) {
        res.setHeader(
          'Cache-Control',
          `s-maxage=31536000, stale-while-revalidate`
        )
      }
J
JJ Kasper 已提交
810
    }
J
JJ Kasper 已提交
811 812 813
    res.end(payload)
  }

814 815 816 817 818 819 820 821 822 823 824 825
  private prepareServerlessUrl(req: IncomingMessage, query: ParsedUrlQuery) {
    const curUrl = parseUrl(req.url!, true)
    req.url = formatUrl({
      ...curUrl,
      search: undefined,
      query: {
        ...curUrl.query,
        ...query,
      },
    })
  }

J
Joe Haddad 已提交
826 827 828 829 830 831
  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
    result: LoadComponentsReturnType,
832
    opts: any
J
JJ Kasper 已提交
833
  ): Promise<string | null> {
834 835 836 837 838
    // we need to ensure the status code if /404 is visited directly
    if (this.nextConfig.experimental.pages404 && pathname === '/404') {
      res.statusCode = 404
    }

J
JJ Kasper 已提交
839
    // handle static page
J
Joe Haddad 已提交
840 841 842 843
    if (typeof result.Component === 'string') {
      return result.Component
    }

J
JJ Kasper 已提交
844 845
    // check request state
    const isLikeServerless =
J
Joe Haddad 已提交
846
      typeof result.Component === 'object' &&
847
      typeof (result.Component as any).renderReqToHTML === 'function'
848
    const isSSG = !!result.unstable_getStaticProps
849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864
    const isServerProps = !!result.unstable_getServerProps

    // Toggle whether or not this is a Data request
    const isDataReq = query._nextDataReq
    delete query._nextDataReq

    // Serverless requests need its URL transformed back into the original
    // request path (to emulate lambda behavior in production)
    if (isLikeServerless && isDataReq) {
      let { pathname } = parseUrl(req.url || '', true)
      pathname = !pathname || pathname === '/' ? '/index' : pathname
      req.url = formatUrl({
        pathname: `/_next/data/${this.buildId}${pathname}.json`,
        query,
      })
    }
J
JJ Kasper 已提交
865 866

    // non-spr requests should render like normal
867
    if (!isSSG) {
J
JJ Kasper 已提交
868 869
      // handle serverless
      if (isLikeServerless) {
870 871 872 873 874 875 876 877 878 879 880 881 882 883 884
        if (isDataReq) {
          const renderResult = await (result.Component as any).renderReqToHTML(
            req,
            res,
            true
          )

          this.__sendPayload(
            res,
            JSON.stringify(renderResult?.renderOpts?.pageData),
            'application/json',
            -1
          )
          return null
        }
885
        this.prepareServerlessUrl(req, query)
886
        return (result.Component as any).renderReqToHTML(req, res)
J
JJ Kasper 已提交
887 888
      }

889 890 891 892 893 894 895 896 897 898
      if (isDataReq && isServerProps) {
        const props = await renderToHTML(req, res, pathname, query, {
          ...result,
          ...opts,
          isDataReq,
        })
        this.__sendPayload(res, JSON.stringify(props), 'application/json', -1)
        return null
      }

J
JJ Kasper 已提交
899 900 901 902 903 904 905
      return renderToHTML(req, res, pathname, query, {
        ...result,
        ...opts,
      })
    }

    // Compute the SPR cache key
906
    const ssgCacheKey = parseUrl(req.url || '').pathname!
J
JJ Kasper 已提交
907 908

    // Complete the response with cached data if its present
909
    const cachedData = await getSprCache(ssgCacheKey)
J
JJ Kasper 已提交
910
    if (cachedData) {
911
      const data = isDataReq
J
JJ Kasper 已提交
912 913 914 915 916 917
        ? JSON.stringify(cachedData.pageData)
        : cachedData.html

      this.__sendPayload(
        res,
        data,
918
        isDataReq ? 'application/json' : 'text/html; charset=utf-8',
J
JJ Kasper 已提交
919
        cachedData.curRevalidate
J
JJ Kasper 已提交
920 921 922 923 924 925
      )

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

J
JJ Kasper 已提交
928 929 930 931
    // If we're here, that means data is missing or it's stale.

    const doRender = withCoalescedInvoke(async function(): Promise<{
      html: string | null
932
      pageData: any
J
JJ Kasper 已提交
933 934
      sprRevalidate: number | false
    }> {
935
      let pageData: any
J
JJ Kasper 已提交
936 937 938 939 940 941
      let html: string | null
      let sprRevalidate: number | false

      let renderResult
      // handle serverless
      if (isLikeServerless) {
942 943 944 945 946
        renderResult = await (result.Component as any).renderReqToHTML(
          req,
          res,
          true
        )
J
JJ Kasper 已提交
947 948

        html = renderResult.html
949
        pageData = renderResult.renderOpts.pageData
J
JJ Kasper 已提交
950 951 952 953 954 955 956 957 958
        sprRevalidate = renderResult.renderOpts.revalidate
      } else {
        const renderOpts = {
          ...result,
          ...opts,
        }
        renderResult = await renderToHTML(req, res, pathname, query, renderOpts)

        html = renderResult
959
        pageData = renderOpts.pageData
J
JJ Kasper 已提交
960 961 962
        sprRevalidate = renderOpts.revalidate
      }

963
      return { html, pageData, sprRevalidate }
964
    })
J
JJ Kasper 已提交
965

966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987
    // render fallback if cached data wasn't available
    if (!isResSent(res) && !isDataReq && isDynamicRoute(pathname)) {
      let html = ''

      if (!this.renderOpts.dev) {
        html = await getFallback(pathname)
      } else {
        query.__nextFallback = 'true'
        if (isLikeServerless) {
          this.prepareServerlessUrl(req, query)
          html = await (result.Component as any).renderReqToHTML(req, res)
        } else {
          html = (await renderToHTML(req, res, pathname, query, {
            ...result,
            ...opts,
          })) as string
        }
      }

      this.__sendPayload(res, html, 'text/html; charset=utf-8')
    }

988 989
    return doRender(ssgCacheKey, []).then(
      async ({ isOrigin, value: { html, pageData, sprRevalidate } }) => {
J
JJ Kasper 已提交
990 991 992 993
        // Respond to the request if a payload wasn't sent above (from cache)
        if (!isResSent(res)) {
          this.__sendPayload(
            res,
994 995
            isDataReq ? JSON.stringify(pageData) : html,
            isDataReq ? 'application/json' : 'text/html; charset=utf-8',
J
JJ Kasper 已提交
996
            sprRevalidate
J
JJ Kasper 已提交
997 998 999 1000 1001 1002
          )
        }

        // Update the SPR cache if the head request
        if (isOrigin) {
          await setSprCache(
1003 1004
            ssgCacheKey,
            { html: html!, pageData },
J
JJ Kasper 已提交
1005 1006 1007 1008 1009 1010 1011
            sprRevalidate
          )
        }

        return null
      }
    )
1012 1013
  }

J
Joe Haddad 已提交
1014
  public renderToHTML(
J
Joe Haddad 已提交
1015 1016 1017 1018
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
J
Joe Haddad 已提交
1019 1020 1021 1022 1023 1024
    {
      amphtml,
      hasAmp,
    }: {
      amphtml?: boolean
      hasAmp?: boolean
1025
    } = {}
J
Joe Haddad 已提交
1026
  ): Promise<string | null> {
J
Joe Haddad 已提交
1027 1028
    return this.findPageComponents(pathname, query)
      .then(
1029
        result => {
J
Joe Haddad 已提交
1030 1031 1032 1033
          return this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
1034
            result.unstable_getStaticProps
1035
              ? { _nextDataReq: query._nextDataReq }
1036
              : query,
J
Joe Haddad 已提交
1037
            result,
T
Tim Neutkens 已提交
1038
            { ...this.renderOpts, amphtml, hasAmp }
J
Joe Haddad 已提交
1039 1040
          )
        },
1041
        err => {
J
Joe Haddad 已提交
1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052
          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(
1053 1054
              result => {
                return this.renderToHTMLWithComponents(
J
Joe Haddad 已提交
1055 1056 1057
                  req,
                  res,
                  dynamicRoute.page,
J
JJ Kasper 已提交
1058 1059 1060
                  // only add params for SPR enabled pages
                  {
                    ...(result.unstable_getStaticProps
1061
                      ? { _nextDataReq: query._nextDataReq }
J
JJ Kasper 已提交
1062 1063 1064
                      : query),
                    ...params,
                  },
J
Joe Haddad 已提交
1065
                  result,
J
JJ Kasper 已提交
1066 1067
                  {
                    ...this.renderOpts,
1068
                    params,
J
JJ Kasper 已提交
1069 1070 1071
                    amphtml,
                    hasAmp,
                  }
1072
                )
1073
              }
J
Joe Haddad 已提交
1074 1075 1076 1077
            )
          }

          return Promise.reject(err)
1078
        }
J
Joe Haddad 已提交
1079
      )
1080
      .catch(err => {
J
Joe Haddad 已提交
1081 1082 1083 1084 1085 1086 1087 1088 1089
        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 已提交
1090 1091
  }

J
Joe Haddad 已提交
1092 1093 1094 1095 1096
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1097
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1098 1099 1100
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
1101
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
1102
    )
N
Naoyuki Kanezawa 已提交
1103
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
1104
    if (html === null) {
1105 1106
      return
    }
1107
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
1108 1109
  }

J
Joe Haddad 已提交
1110 1111 1112 1113 1114
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
1115
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1116
  ) {
1117 1118
    let result: null | LoadComponentsReturnType = null

1119 1120 1121 1122
    const { static404, pages404 } = this.nextConfig.experimental
    const is404 = res.statusCode === 404
    let using404Page = false

1123
    // use static 404 page if available and is 404 response
1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144
    if (is404) {
      if (static404) {
        try {
          result = await this.findPageComponents('/_errors/404')
        } catch (err) {
          if (err.code !== 'ENOENT') {
            throw err
          }
        }
      }

      // use 404 if /_errors/404 isn't available which occurs
      // during development and when _app has getInitialProps
      if (!result && pages404) {
        try {
          result = await this.findPageComponents('/404')
          using404Page = true
        } catch (err) {
          if (err.code !== 'ENOENT') {
            throw err
          }
1145 1146 1147 1148 1149 1150 1151 1152
        }
      }
    }

    if (!result) {
      result = await this.findPageComponents('/_error', query)
    }

1153 1154 1155 1156 1157
    let html
    try {
      html = await this.renderToHTMLWithComponents(
        req,
        res,
1158
        using404Page ? '/404' : '/_error',
1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171
        query,
        result,
        {
          ...this.renderOpts,
          err,
        }
      )
    } catch (err) {
      console.error(err)
      res.statusCode = 500
      html = 'Internal Server Error'
    }
    return html
N
Naoyuki Kanezawa 已提交
1172 1173
  }

J
Joe Haddad 已提交
1174 1175 1176
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
1177
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1178
  ): Promise<void> {
1179 1180
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
1181
    if (!pathname) {
1182 1183
      throw new Error('pathname is undefined')
    }
N
Naoyuki Kanezawa 已提交
1184
    res.statusCode = 404
1185
    return this.renderError(null, req, res, pathname, query)
N
Naoyuki Kanezawa 已提交
1186
  }
N
Naoyuki Kanezawa 已提交
1187

J
Joe Haddad 已提交
1188 1189 1190 1191
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
1192
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1193
  ): Promise<void> {
A
Arunoda Susiripala 已提交
1194
    if (!this.isServeableUrl(path)) {
1195
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
1196 1197
    }

1198 1199 1200 1201 1202 1203
    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 已提交
1204
    try {
1205
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
1206
    } catch (err) {
T
Tim Neutkens 已提交
1207
      if (err.code === 'ENOENT' || err.statusCode === 404) {
1208
        this.render404(req, res, parsedUrl)
1209 1210 1211
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
1212 1213 1214 1215 1216 1217
      } else {
        throw err
      }
    }
  }

1218
  private isServeableUrl(path: string): boolean {
A
Arunoda Susiripala 已提交
1219 1220
    const resolved = resolve(path)
    if (
1221
      resolved.indexOf(join(this.distDir) + sep) !== 0 &&
1222 1223
      resolved.indexOf(join(this.dir, 'static') + sep) !== 0 &&
      resolved.indexOf(join(this.dir, 'public') + sep) !== 0
A
Arunoda Susiripala 已提交
1224 1225 1226 1227 1228 1229 1230 1231
    ) {
      // Seems like the user is trying to traverse the filesystem.
      return false
    }

    return true
  }

1232
  protected readBuildId(): string {
1233 1234 1235 1236 1237
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
1238
        throw new Error(
1239
          `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 已提交
1240
        )
1241 1242 1243
      }

      throw err
1244
    }
1245
  }
1246 1247 1248 1249

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