next-server.ts 44.1 KB
Newer Older
G
Guy Bedford 已提交
1
import compression from 'next/dist/compiled/compression'
J
Joe Haddad 已提交
2
import fs from 'fs'
3
import chalk from 'next/dist/compiled/chalk'
J
Joe Haddad 已提交
4
import { IncomingMessage, ServerResponse } from 'http'
G
Guy Bedford 已提交
5
import Proxy from 'next/dist/compiled/http-proxy'
6
import { join, relative, resolve, sep } from 'path'
7
import { parse as parseQs, ParsedUrlQuery } from 'querystring'
8
import { format as formatUrl, parse as parseUrl, UrlWithParsedQuery } from 'url'
9
import { PrerenderManifest } from '../../build'
J
Joe Haddad 已提交
10 11 12 13 14 15
import {
  getRedirectStatus,
  Header,
  Redirect,
  Rewrite,
  RouteType,
16 17
  CustomRoutes,
} from '../../lib/load-custom-routes'
J
JJ Kasper 已提交
18
import { withCoalescedInvoke } from '../../lib/coalesced-function'
J
Joe Haddad 已提交
19 20
import {
  BUILD_ID_FILE,
21
  CLIENT_PUBLIC_FILES_PATH,
J
Joe Haddad 已提交
22 23
  CLIENT_STATIC_FILES_PATH,
  CLIENT_STATIC_FILES_RUNTIME,
24
  PAGES_MANIFEST,
J
Joe Haddad 已提交
25
  PHASE_PRODUCTION_SERVER,
J
Joe Haddad 已提交
26
  PRERENDER_MANIFEST,
27
  ROUTES_MANIFEST,
28
  SERVERLESS_DIRECTORY,
J
Joe Haddad 已提交
29
  SERVER_DIRECTORY,
T
Tim Neutkens 已提交
30
} from '../lib/constants'
J
Joe Haddad 已提交
31 32 33 34
import {
  getRouteMatcher,
  getRouteRegex,
  getSortedRoutes,
35
  isDynamicRoute,
J
Joe Haddad 已提交
36
} from '../lib/router/utils'
37
import * as envConfig from '../lib/runtime-config'
J
Joe Haddad 已提交
38
import { isResSent, NextApiRequest, NextApiResponse } from '../lib/utils'
J
Joe Haddad 已提交
39
import { apiResolver, tryGetPreviewData, __ApiPreviewProps } from './api-utils'
40
import loadConfig, { isTargetLikeServerless } from './config'
41
import pathMatch from '../lib/router/utils/path-match'
J
Joe Haddad 已提交
42
import { recursiveReadDirSync } from './lib/recursive-readdir-sync'
43
import { loadComponents, LoadComponentsReturnType } from './load-components'
J
Joe Haddad 已提交
44
import { normalizePagePath } from './normalize-page-path'
45
import { RenderOpts, RenderOptsPartial, renderToHTML } from './render'
P
Prateek Bhatnagar 已提交
46
import { getPagePath, requireFontManifest } from './require'
47 48 49
import Router, {
  DynamicRoutes,
  PageChecker,
J
Joe Haddad 已提交
50 51 52
  Params,
  route,
  Route,
53
} from './router'
54
import prepareDestination from '../lib/router/utils/prepare-destination'
55
import { sendPayload } from './send-payload'
J
Joe Haddad 已提交
56
import { serveStatic } from './serve-static'
57
import { IncrementalCache } from './incremental-cache'
58
import { execOnce } from '../lib/utils'
59
import { isBlockedPage } from './utils'
60
import { compile as compilePathToRegex } from 'next/dist/compiled/path-to-regexp'
61
import { loadEnvConfig } from '../../lib/load-env-config'
62
import './node-polyfill-fetch'
J
Jan Potoms 已提交
63
import { PagesManifest } from '../../build/webpack/plugins/pages-manifest-plugin'
64
import { removePathTrailingSlash } from '../../client/normalize-trailing-slash'
65
import getRouteFromAssetPath from '../lib/router/utils/get-route-from-asset-path'
P
Prateek Bhatnagar 已提交
66
import { FontManifest } from './font-utils'
67
import { denormalizePagePath } from './denormalize-page-path'
J
JJ Kasper 已提交
68 69

const getCustomRouteMatcher = pathMatch(true)
70 71 72

type NextConfig = any

73 74 75 76 77 78
type Middleware = (
  req: IncomingMessage,
  res: ServerResponse,
  next: (err?: Error) => void
) => void

79 80 81 82 83
type FindComponentsResult = {
  components: LoadComponentsReturnType
  query: ParsedUrlQuery
}

T
Tim Neutkens 已提交
84
export type ServerConstructor = {
85 86 87
  /**
   * Where the Next project is located - @default '.'
   */
J
Joe Haddad 已提交
88
  dir?: string
89 90 91
  /**
   * Hide error messages containing server information - @default false
   */
J
Joe Haddad 已提交
92
  quiet?: boolean
93 94 95
  /**
   * Object what you would use in next.config.js - @default {}
   */
96
  conf?: NextConfig
J
JJ Kasper 已提交
97
  dev?: boolean
98
  customServer?: boolean
99
}
100

N
nkzawa 已提交
101
export default class Server {
102 103 104 105
  dir: string
  quiet: boolean
  nextConfig: NextConfig
  distDir: string
106
  pagesDir?: string
107
  publicDir: string
108
  hasStaticDir: boolean
109
  serverBuildDir: string
J
Jan Potoms 已提交
110
  pagesManifest?: PagesManifest
111 112
  buildId: string
  renderOpts: {
T
Tim Neutkens 已提交
113
    poweredByHeader: boolean
J
Joe Haddad 已提交
114 115 116
    buildId: string
    generateEtags: boolean
    runtimeConfig?: { [key: string]: any }
117 118 119
    assetPrefix?: string
    canonicalBase: string
    dev?: boolean
120
    previewProps: __ApiPreviewProps
121
    customServer?: boolean
122
    ampOptimizerConfig?: { [key: string]: any }
123
    basePath: string
P
Prateek Bhatnagar 已提交
124 125
    optimizeFonts: boolean
    fontManifest: FontManifest
126
    optimizeImages: boolean
127
  }
128
  private compression?: Middleware
J
JJ Kasper 已提交
129
  private onErrorMiddleware?: ({ err }: { err: Error }) => Promise<void>
130
  private incrementalCache: IncrementalCache
131
  router: Router
132
  protected dynamicRoutes?: DynamicRoutes
133
  protected customRoutes: CustomRoutes
134

J
Joe Haddad 已提交
135 136 137 138
  public constructor({
    dir = '.',
    quiet = false,
    conf = null,
J
JJ Kasper 已提交
139
    dev = false,
140
    customServer = true,
J
Joe Haddad 已提交
141
  }: ServerConstructor = {}) {
N
nkzawa 已提交
142
    this.dir = resolve(dir)
N
Naoyuki Kanezawa 已提交
143
    this.quiet = quiet
T
Tim Neutkens 已提交
144
    const phase = this.currentPhase()
145
    loadEnvConfig(this.dir, dev)
146

147
    this.nextConfig = loadConfig(phase, this.dir, conf)
148
    this.distDir = join(this.dir, this.nextConfig.distDir)
149
    this.publicDir = join(this.dir, CLIENT_PUBLIC_FILES_PATH)
150
    this.hasStaticDir = fs.existsSync(join(this.dir, 'static'))
T
Tim Neutkens 已提交
151

152 153
    // Only serverRuntimeConfig needs the default
    // publicRuntimeConfig gets it's default in client/index.js
J
Joe Haddad 已提交
154 155 156 157 158
    const {
      serverRuntimeConfig = {},
      publicRuntimeConfig,
      assetPrefix,
      generateEtags,
159
      compress,
J
Joe Haddad 已提交
160
    } = this.nextConfig
161

T
Tim Neutkens 已提交
162
    this.buildId = this.readBuildId()
163

164
    this.renderOpts = {
T
Tim Neutkens 已提交
165
      poweredByHeader: this.nextConfig.poweredByHeader,
166
      canonicalBase: this.nextConfig.amp.canonicalBase,
167
      buildId: this.buildId,
168
      generateEtags,
169
      previewProps: this.getPreviewProps(),
170
      customServer: customServer === true ? true : undefined,
171
      ampOptimizerConfig: this.nextConfig.experimental.amp?.optimizer,
172
      basePath: this.nextConfig.basePath,
173 174 175 176 177
      optimizeFonts: this.nextConfig.experimental.optimizeFonts && !dev,
      fontManifest:
        this.nextConfig.experimental.optimizeFonts && !dev
          ? requireFontManifest(this.distDir, this._isLikeServerless)
          : null,
178
      optimizeImages: this.nextConfig.experimental.optimizeImages,
179
    }
N
Naoyuki Kanezawa 已提交
180

181 182
    // Only the `publicRuntimeConfig` key is exposed to the client side
    // It'll be rendered as part of __NEXT_DATA__ on the client side
183
    if (Object.keys(publicRuntimeConfig).length > 0) {
184
      this.renderOpts.runtimeConfig = publicRuntimeConfig
185 186
    }

187
    if (compress && this.nextConfig.target === 'server') {
188 189 190
      this.compression = compression() as Middleware
    }

191
    // Initialize next/config with the environment configuration
192 193 194 195
    envConfig.setConfig({
      serverRuntimeConfig,
      publicRuntimeConfig,
    })
196

197 198 199 200 201 202 203 204 205 206
    this.serverBuildDir = join(
      this.distDir,
      this._isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY
    )
    const pagesManifestPath = join(this.serverBuildDir, PAGES_MANIFEST)

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

207
    this.customRoutes = this.getCustomRoutes()
J
JJ Kasper 已提交
208
    this.router = new Router(this.generateRoutes())
209
    this.setAssetPrefix(assetPrefix)
J
JJ Kasper 已提交
210

211 212 213
    // call init-server middleware, this is also handled
    // individually in serverless bundles when deployed
    if (!dev && this.nextConfig.experimental.plugins) {
214 215
      const initServer = require(join(this.serverBuildDir, 'init-server.js'))
        .default
216
      this.onErrorMiddleware = require(join(
217
        this.serverBuildDir,
218 219 220 221 222
        'on-error-server.js'
      )).default
      initServer()
    }

223
    this.incrementalCache = new IncrementalCache({
J
JJ Kasper 已提交
224 225 226 227
      dev,
      distDir: this.distDir,
      pagesDir: join(
        this.distDir,
228
        this._isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY,
J
JJ Kasper 已提交
229 230 231 232
        'pages'
      ),
      flushToDisk: this.nextConfig.experimental.sprFlushToDisk,
    })
P
Prateek Bhatnagar 已提交
233 234 235 236 237 238 239 240 241 242

    /**
     * This sets environment variable to be used at the time of SSR by head.tsx.
     * Using this from process.env allows targetting both serverless and SSR by calling
     * `process.env.__NEXT_OPTIMIZE_FONTS`.
     * TODO(prateekbh@): Remove this when experimental.optimizeFonts are being clened up.
     */
    if (this.renderOpts.optimizeFonts) {
      process.env.__NEXT_OPTIMIZE_FONTS = JSON.stringify(true)
    }
243 244 245
    if (this.renderOpts.optimizeImages) {
      process.env.__NEXT_OPTIMIZE_IMAGES = JSON.stringify(true)
    }
N
Naoyuki Kanezawa 已提交
246
  }
N
nkzawa 已提交
247

248
  protected currentPhase(): string {
249
    return PHASE_PRODUCTION_SERVER
250 251
  }

252 253 254 255
  private logError(err: Error): void {
    if (this.onErrorMiddleware) {
      this.onErrorMiddleware({ err })
    }
256
    if (this.quiet) return
257
    console.error(err)
258 259
  }

260
  private async handleRequest(
J
Joe Haddad 已提交
261 262
    req: IncomingMessage,
    res: ServerResponse,
263
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
264
  ): Promise<void> {
265
    // Parse url if parsedUrl not provided
266
    if (!parsedUrl || typeof parsedUrl !== 'object') {
267 268
      const url: any = req.url
      parsedUrl = parseUrl(url, true)
269
    }
270

271 272 273
    // 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 已提交
274
    }
275

276
    const { basePath } = this.nextConfig
277

278 279 280 281 282
    if (basePath && req.url?.startsWith(basePath)) {
      // store original URL to allow checking if basePath was
      // provided or not
      ;(req as any)._nextHadBasePath = true
      req.url = req.url!.replace(basePath, '') || '/'
T
Tim Neutkens 已提交
283 284
    }

285
    res.statusCode = 200
286 287 288
    try {
      return await this.run(req, res, parsedUrl)
    } catch (err) {
J
Joe Haddad 已提交
289 290 291
      this.logError(err)
      res.statusCode = 500
      res.end('Internal Server Error')
292
    }
293 294
  }

295
  public getRequestHandler() {
296
    return this.handleRequest.bind(this)
N
nkzawa 已提交
297 298
  }

299
  public setAssetPrefix(prefix?: string): void {
300
    this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
301 302
  }

303
  // Backwards compatibility
304
  public async prepare(): Promise<void> {}
N
nkzawa 已提交
305

T
Tim Neutkens 已提交
306
  // Backwards compatibility
307
  protected async close(): Promise<void> {}
T
Tim Neutkens 已提交
308

309
  protected setImmutableAssetCacheControl(res: ServerResponse): void {
T
Tim Neutkens 已提交
310
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
N
nkzawa 已提交
311 312
  }

313
  protected getCustomRoutes(): CustomRoutes {
J
JJ Kasper 已提交
314 315 316
    return require(join(this.distDir, ROUTES_MANIFEST))
  }

317 318 319 320
  private _cachedPreviewManifest: PrerenderManifest | undefined
  protected getPrerenderManifest(): PrerenderManifest {
    if (this._cachedPreviewManifest) {
      return this._cachedPreviewManifest
J
Joe Haddad 已提交
321
    }
322 323 324 325 326 327
    const manifest = require(join(this.distDir, PRERENDER_MANIFEST))
    return (this._cachedPreviewManifest = manifest)
  }

  protected getPreviewProps(): __ApiPreviewProps {
    return this.getPrerenderManifest().preview
J
Joe Haddad 已提交
328 329
  }

330
  protected generateRoutes(): {
331
    basePath: string
332 333
    headers: Route[]
    rewrites: Route[]
334
    fsRoutes: Route[]
335
    redirects: Route[]
336 337
    catchAllRoute: Route
    pageChecker: PageChecker
338
    useFileSystemPublicRoutes: boolean
339 340
    dynamicRoutes: DynamicRoutes | undefined
  } {
341 342 343
    const publicRoutes = fs.existsSync(this.publicDir)
      ? this.generatePublicRoutes()
      : []
J
JJ Kasper 已提交
344

345
    const staticFilesRoute = this.hasStaticDir
346 347 348 349 350
      ? [
          {
            // 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.
351
            // See more: https://github.com/vercel/next.js/issues/2617
352
            match: route('/static/:path*'),
353
            name: 'static catchall',
354
            fn: async (req, res, params, parsedUrl) => {
355 356 357 358 359
              const p = join(
                this.dir,
                'static',
                ...(params.path || []).map(encodeURIComponent)
              )
360
              await this.serveStatic(req, res, p, parsedUrl)
361 362 363
              return {
                finished: true,
              }
364 365 366 367
            },
          } as Route,
        ]
      : []
368

369
    const fsRoutes: Route[] = [
T
Tim Neutkens 已提交
370
      {
371
        match: route('/_next/static/:path*'),
372 373
        type: 'route',
        name: '_next/static catchall',
374
        fn: async (req, res, params, parsedUrl) => {
375
          // make sure to 404 for /_next/static itself
376 377 378 379 380 381
          if (!params.path) {
            await this.render404(req, res, parsedUrl)
            return {
              finished: true,
            }
          }
382

J
Joe Haddad 已提交
383 384 385
          if (
            params.path[0] === CLIENT_STATIC_FILES_RUNTIME ||
            params.path[0] === 'chunks' ||
386 387
            params.path[0] === 'css' ||
            params.path[0] === 'media' ||
388
            params.path[0] === this.buildId ||
389
            params.path[0] === 'pages' ||
390
            params.path[1] === 'pages'
J
Joe Haddad 已提交
391
          ) {
T
Tim Neutkens 已提交
392
            this.setImmutableAssetCacheControl(res)
393
          }
J
Joe Haddad 已提交
394 395 396
          const p = join(
            this.distDir,
            CLIENT_STATIC_FILES_PATH,
397
            ...(params.path || [])
J
Joe Haddad 已提交
398
          )
399
          await this.serveStatic(req, res, p, parsedUrl)
400 401 402
          return {
            finished: true,
          }
403
        },
404
      },
J
JJ Kasper 已提交
405 406
      {
        match: route('/_next/data/:path*'),
407 408
        type: 'route',
        name: '_next/data catchall',
J
JJ Kasper 已提交
409
        fn: async (req, res, params, _parsedUrl) => {
J
JJ Kasper 已提交
410 411 412
          // 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) {
413 414 415 416
            await this.render404(req, res, _parsedUrl)
            return {
              finished: true,
            }
J
JJ Kasper 已提交
417 418 419 420 421 422
          }
          // 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')) {
423 424 425 426
            await this.render404(req, res, _parsedUrl)
            return {
              finished: true,
            }
J
JJ Kasper 已提交
427 428 429
          }

          // re-create page's pathname
430 431 432 433 434 435 436 437
          const pathname = getRouteFromAssetPath(
            `/${params.path
              // we need to re-encode the params since they are decoded
              // by path-match and we are re-building the URL
              .map((param: string) => encodeURIComponent(param))
              .join('/')}`,
            '.json'
          )
J
JJ Kasper 已提交
438

J
JJ Kasper 已提交
439
          const parsedUrl = parseUrl(pathname, true)
440

J
JJ Kasper 已提交
441 442 443 444
          await this.render(
            req,
            res,
            pathname,
445
            { ..._parsedUrl.query, _nextDataReq: '1' },
J
JJ Kasper 已提交
446 447
            parsedUrl
          )
448 449 450
          return {
            finished: true,
          }
J
JJ Kasper 已提交
451 452
        },
      },
T
Tim Neutkens 已提交
453
      {
454
        match: route('/_next/:path*'),
455 456
        type: 'route',
        name: '_next catchall',
T
Tim Neutkens 已提交
457
        // This path is needed because `render()` does a check for `/_next` and the calls the routing again
458
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
459
          await this.render404(req, res, parsedUrl)
460 461 462
          return {
            finished: true,
          }
L
Lukáš Huvar 已提交
463 464
        },
      },
465 466
      ...publicRoutes,
      ...staticFilesRoute,
T
Tim Neutkens 已提交
467
    ]
468

469 470 471 472 473 474
    const getCustomRouteBasePath = (r: { basePath?: false }) => {
      return r.basePath !== false && this.renderOpts.dev
        ? this.nextConfig.basePath
        : ''
    }

475 476 477 478
    const getCustomRoute = (r: Rewrite | Redirect | Header, type: RouteType) =>
      ({
        ...r,
        type,
479
        match: getCustomRouteMatcher(`${getCustomRouteBasePath(r)}${r.source}`),
480 481 482 483 484 485 486
        name: type,
        fn: async (_req, _res, _params, _parsedUrl) => ({ finished: false }),
      } as Route & Rewrite & Header)

    const updateHeaderValue = (value: string, params: Params): string => {
      if (!value.includes(':')) {
        return value
487
      }
488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508

      for (const key of Object.keys(params)) {
        if (value.includes(`:${key}`)) {
          value = value
            .replace(
              new RegExp(`:${key}\\*`, 'g'),
              `:${key}--ESCAPED_PARAM_ASTERISKS`
            )
            .replace(
              new RegExp(`:${key}\\?`, 'g'),
              `:${key}--ESCAPED_PARAM_QUESTION`
            )
            .replace(
              new RegExp(`:${key}\\+`, 'g'),
              `:${key}--ESCAPED_PARAM_PLUS`
            )
            .replace(
              new RegExp(`:${key}(?!\\w)`, 'g'),
              `--ESCAPED_PARAM_COLON${key}`
            )
        }
509
      }
510 511 512 513 514 515 516 517 518 519 520 521
      value = value
        .replace(/(:|\*|\?|\+|\(|\)|\{|\})/g, '\\$1')
        .replace(/--ESCAPED_PARAM_PLUS/g, '+')
        .replace(/--ESCAPED_PARAM_COLON/g, ':')
        .replace(/--ESCAPED_PARAM_QUESTION/g, '?')
        .replace(/--ESCAPED_PARAM_ASTERISKS/g, '*')

      // the value needs to start with a forward-slash to be compiled
      // correctly
      return compilePathToRegex(`/${value}`, { validate: false })(
        params
      ).substr(1)
522
    }
523

524 525 526 527 528 529 530 531 532 533 534 535 536 537 538
    // Headers come very first
    const headers = this.customRoutes.headers.map((r) => {
      const headerRoute = getCustomRoute(r, 'header')
      return {
        match: headerRoute.match,
        type: headerRoute.type,
        name: `${headerRoute.type} ${headerRoute.source} header route`,
        fn: async (_req, res, params, _parsedUrl) => {
          const hasParams = Object.keys(params).length > 0

          for (const header of (headerRoute as Header).headers) {
            let { key, value } = header
            if (hasParams) {
              key = updateHeaderValue(key, params)
              value = updateHeaderValue(value, params)
539
            }
540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557
            res.setHeader(key, value)
          }
          return { finished: false }
        },
      } as Route
    })

    const redirects = this.customRoutes.redirects.map((redirect) => {
      const redirectRoute = getCustomRoute(redirect, 'redirect')
      return {
        type: redirectRoute.type,
        match: redirectRoute.match,
        statusCode: redirectRoute.statusCode,
        name: `Redirect route`,
        fn: async (_req, res, params, parsedUrl) => {
          const { parsedDestination } = prepareDestination(
            redirectRoute.destination,
            params,
558 559 560
            parsedUrl.query,
            false,
            getCustomRouteBasePath(redirectRoute)
561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583
          )
          const updatedDestination = formatUrl(parsedDestination)

          res.setHeader('Location', updatedDestination)
          res.statusCode = getRedirectStatus(redirectRoute 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}`)
          }

          res.end()
          return {
            finished: true,
          }
        },
      } as Route
    })

    const rewrites = this.customRoutes.rewrites.map((rewrite) => {
      const rewriteRoute = getCustomRoute(rewrite, 'rewrite')
      return {
584
        ...rewriteRoute,
585 586 587 588 589 590 591 592 593
        check: true,
        type: rewriteRoute.type,
        name: `Rewrite route`,
        match: rewriteRoute.match,
        fn: async (req, res, params, parsedUrl) => {
          const { newUrl, parsedDestination } = prepareDestination(
            rewriteRoute.destination,
            params,
            parsedUrl.query,
594 595
            true,
            getCustomRouteBasePath(rewriteRoute)
596
          )
597

598 599 600 601 602 603 604 605 606 607 608 609 610
          // external rewrite, proxy it
          if (parsedDestination.protocol) {
            const target = formatUrl(parsedDestination)
            const proxy = new Proxy({
              target,
              changeOrigin: true,
              ignorePath: true,
            })
            proxy.web(req, res)

            proxy.on('error', (err: Error) => {
              console.error(`Error occurred proxying ${target}`, err)
            })
611 612 613
            return {
              finished: true,
            }
614 615
          }
          ;(req as any)._nextRewroteUrl = newUrl
616 617
          ;(req as any)._nextDidRewrite =
            (req as any)._nextRewroteUrl !== req.url
618

619 620 621 622 623 624 625 626
          return {
            finished: false,
            pathname: newUrl,
            query: parsedDestination.query,
          }
        },
      } as Route
    })
627 628 629 630 631 632

    const catchAllRoute: Route = {
      match: route('/:path*'),
      type: 'route',
      name: 'Catchall render',
      fn: async (req, res, params, parsedUrl) => {
J
Jan Potoms 已提交
633
        let { pathname, query } = parsedUrl
634 635 636 637
        if (!pathname) {
          throw new Error('pathname is undefined')
        }

J
Jan Potoms 已提交
638
        // next.js core assumes page path without trailing slash
639
        pathname = removePathTrailingSlash(pathname)
J
Jan Potoms 已提交
640

641
        if (params?.path?.[0] === 'api') {
642 643 644
          const handled = await this.handleApiRequest(
            req as NextApiRequest,
            res as NextApiResponse,
645
            pathname,
646
            query
647 648 649 650 651 652 653
          )
          if (handled) {
            return { finished: true }
          }
        }

        await this.render(req, res, pathname, query, parsedUrl)
654 655 656 657
        return {
          finished: true,
        }
      },
658
    }
659

660
    const { useFileSystemPublicRoutes } = this.nextConfig
J
Joe Haddad 已提交
661

662 663
    if (useFileSystemPublicRoutes) {
      this.dynamicRoutes = this.getDynamicRoutes()
664
    }
N
nkzawa 已提交
665

666
    return {
667
      headers,
668
      fsRoutes,
669 670
      rewrites,
      redirects,
671
      catchAllRoute,
672
      useFileSystemPublicRoutes,
673
      dynamicRoutes: this.dynamicRoutes,
674
      basePath: this.nextConfig.basePath,
675 676
      pageChecker: this.hasPage.bind(this),
    }
T
Tim Neutkens 已提交
677 678
  }

679
  private async getPagePath(pathname: string): Promise<string> {
680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696
    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
  }

697 698 699 700 701
  protected async _beforeCatchAllRender(
    _req: IncomingMessage,
    _res: ServerResponse,
    _params: Params,
    _parsedUrl: UrlWithParsedQuery
702
  ): Promise<boolean> {
703 704 705
    return false
  }

706
  // Used to build API page in development
T
Tim Neutkens 已提交
707
  protected async ensureApiPage(_pathname: string): Promise<void> {}
708

L
Lukáš Huvar 已提交
709 710 711 712 713 714
  /**
   * Resolves `API` request, in development builds on demand
   * @param req http request
   * @param res http response
   * @param pathname path of request
   */
J
Joe Haddad 已提交
715
  private async handleApiRequest(
716 717
    req: IncomingMessage,
    res: ServerResponse,
718 719
    pathname: string,
    query: ParsedUrlQuery
720
  ): Promise<boolean> {
721
    let page = pathname
L
Lukáš Huvar 已提交
722
    let params: Params | boolean = false
723
    let pageFound = await this.hasPage(page)
J
JJ Kasper 已提交
724

725
    if (!pageFound && this.dynamicRoutes) {
L
Lukáš Huvar 已提交
726 727
      for (const dynamicRoute of this.dynamicRoutes) {
        params = dynamicRoute.match(pathname)
728
        if (dynamicRoute.page.startsWith('/api') && params) {
729 730
          page = dynamicRoute.page
          pageFound = true
L
Lukáš Huvar 已提交
731 732 733 734 735
          break
        }
      }
    }

736
    if (!pageFound) {
737
      return false
J
JJ Kasper 已提交
738
    }
739 740 741 742
    // Make sure the page is built before getting the path
    // or else it won't be in the manifest yet
    await this.ensureApiPage(page)

743 744 745 746 747 748 749 750 751 752
    let builtPagePath
    try {
      builtPagePath = await this.getPagePath(page)
    } catch (err) {
      if (err.code === 'ENOENT') {
        return false
      }
      throw err
    }

753
    const pageModule = require(builtPagePath)
754
    query = { ...query, ...params }
J
JJ Kasper 已提交
755

756
    if (!this.renderOpts.dev && this._isLikeServerless) {
757
      if (typeof pageModule.default === 'function') {
758
        prepareServerlessUrl(req, query)
759 760
        await pageModule.default(req, res)
        return true
J
JJ Kasper 已提交
761 762 763
      }
    }

J
Joe Haddad 已提交
764 765 766 767 768
    await apiResolver(
      req,
      res,
      query,
      pageModule,
769
      this.renderOpts.previewProps,
770
      false,
J
Joe Haddad 已提交
771 772
      this.onErrorMiddleware
    )
773
    return true
L
Lukáš Huvar 已提交
774 775
  }

776
  protected generatePublicRoutes(): Route[] {
777
    const publicFiles = new Set(
J
Joe Haddad 已提交
778
      recursiveReadDirSync(this.publicDir).map((p) => p.replace(/\\/g, '/'))
779 780 781 782 783 784 785
    )

    return [
      {
        match: route('/:path*'),
        name: 'public folder catchall',
        fn: async (req, res, params, parsedUrl) => {
786
          const pathParts: string[] = params.path || []
787 788 789 790 791 792 793 794
          const { basePath } = this.nextConfig

          // if basePath is defined require it be present
          if (basePath) {
            if (pathParts[0] !== basePath.substr(1)) return { finished: false }
            pathParts.shift()
          }

795
          const path = `/${pathParts.join('/')}`
796 797 798 799 800 801

          if (publicFiles.has(path)) {
            await this.serveStatic(
              req,
              res,
              // we need to re-encode it since send decodes it
802
              join(this.publicDir, ...pathParts.map(encodeURIComponent)),
803 804
              parsedUrl
            )
805 806 807
            return {
              finished: true,
            }
808 809 810 811 812 813 814
          }
          return {
            finished: false,
          }
        },
      } as Route,
    ]
815 816
  }

817
  protected getDynamicRoutes() {
818 819
    return getSortedRoutes(Object.keys(this.pagesManifest!))
      .filter(isDynamicRoute)
J
Joe Haddad 已提交
820
      .map((page) => ({
821 822 823
        page,
        match: getRouteMatcher(getRouteRegex(page)),
      }))
J
Joe Haddad 已提交
824 825
  }

826
  private handleCompression(req: IncomingMessage, res: ServerResponse): void {
827 828 829 830 831
    if (this.compression) {
      this.compression(req, res, () => {})
    }
  }

832
  protected async run(
J
Joe Haddad 已提交
833 834
    req: IncomingMessage,
    res: ServerResponse,
835
    parsedUrl: UrlWithParsedQuery
836
  ): Promise<void> {
837 838
    this.handleCompression(req, res)

839
    try {
840 841
      const matched = await this.router.execute(req, res, parsedUrl)
      if (matched) {
842 843 844 845 846 847 848 849
        return
      }
    } catch (err) {
      if (err.code === 'DECODE_FAILED') {
        res.statusCode = 400
        return this.renderError(null, req, res, '/_error', {})
      }
      throw err
850 851
    }

852
    await this.render404(req, res, parsedUrl)
N
nkzawa 已提交
853 854
  }

855
  protected async sendHTML(
J
Joe Haddad 已提交
856 857
    req: IncomingMessage,
    res: ServerResponse,
858
    html: string
859
  ): Promise<void> {
T
Tim Neutkens 已提交
860
    const { generateEtags, poweredByHeader } = this.renderOpts
861 862 863 864
    return sendPayload(req, res, html, 'html', {
      generateEtags,
      poweredByHeader,
    })
865 866
  }

J
Joe Haddad 已提交
867 868 869 870 871
  public async render(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
872
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
873
  ): Promise<void> {
874 875 876 877 878 879
    if (!pathname.startsWith('/')) {
      console.warn(
        `Cannot render page with path "${pathname}", did you mean "/${pathname}"?. See more info here: https://err.sh/next.js/render-no-starting-slash`
      )
    }

880 881 882 883 884 885 886 887 888 889
    if (
      this.renderOpts.customServer &&
      pathname === '/index' &&
      !(await this.hasPage('/index'))
    ) {
      // maintain backwards compatibility for custom server
      // (see custom-server integration tests)
      pathname = '/'
    }

890
    const url: any = req.url
891

892 893 894 895
    // we allow custom servers to call render for all URLs
    // so check if we need to serve a static _next file or not.
    // we don't modify the URL for _next/data request but still
    // call render so we special case this to prevent an infinite loop
896
    if (
897 898 899
      !query._nextDataReq &&
      (url.match(/^\/_next\//) ||
        (this.hasStaticDir && url.match(/^\/static\//)))
900
    ) {
901 902 903
      return this.handleRequest(req, res, parsedUrl)
    }

904
    if (isBlockedPage(pathname)) {
905
      return this.render404(req, res, parsedUrl)
906 907
    }

908
    const html = await this.renderToHTML(req, res, pathname, query)
909 910
    // Request was ended by the user
    if (html === null) {
911 912 913
      return
    }

914
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
915
  }
N
nkzawa 已提交
916

J
Joe Haddad 已提交
917
  private async findPageComponents(
J
Joe Haddad 已提交
918
    pathname: string,
919 920 921 922 923 924 925 926 927
    query: ParsedUrlQuery = {},
    params: Params | null = null
  ): Promise<FindComponentsResult | null> {
    const paths = [
      // try serving a static AMP version first
      query.amp ? normalizePagePath(pathname) + '.amp' : null,
      pathname,
    ].filter(Boolean)
    for (const pagePath of paths) {
J
JJ Kasper 已提交
928
      try {
929
        const components = await loadComponents(
J
Joe Haddad 已提交
930
          this.distDir,
931 932
          pagePath!,
          !this.renderOpts.dev && this._isLikeServerless
J
Joe Haddad 已提交
933
        )
934 935 936
        return {
          components,
          query: {
937
            ...(components.getStaticProps
938
              ? { _nextDataReq: query._nextDataReq, amp: query.amp }
939 940 941 942
              : query),
            ...(params || {}),
          },
        }
J
JJ Kasper 已提交
943 944 945 946
      } catch (err) {
        if (err.code !== 'ENOENT') throw err
      }
    }
947
    return null
J
Joe Haddad 已提交
948 949
  }

950
  protected async getStaticPaths(
951 952 953
    pathname: string
  ): Promise<{
    staticPaths: string[] | undefined
954
    fallbackMode: 'static' | 'blocking' | false
955
  }> {
956 957 958 959 960
    // `staticPaths` is intentionally set to `undefined` as it should've
    // been caught when checking disk data.
    const staticPaths = undefined

    // Read whether or not fallback should exist from the manifest.
961 962
    const fallbackField = this.getPrerenderManifest().dynamicRoutes[pathname]
      .fallback
963

964 965 966 967 968 969 970 971 972
    return {
      staticPaths,
      fallbackMode:
        typeof fallbackField === 'string'
          ? 'static'
          : fallbackField === null
          ? 'blocking'
          : false,
    }
973 974
  }

J
Joe Haddad 已提交
975 976 977 978
  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
979
    { components, query }: FindComponentsResult,
980
    opts: RenderOptsPartial
981
  ): Promise<string | null> {
982
    // we need to ensure the status code if /404 is visited directly
983
    if (pathname === '/404') {
984 985 986
      res.statusCode = 404
    }

J
JJ Kasper 已提交
987
    // handle static page
988 989
    if (typeof components.Component === 'string') {
      return components.Component
J
Joe Haddad 已提交
990 991
    }

J
JJ Kasper 已提交
992 993
    // check request state
    const isLikeServerless =
994 995
      typeof components.Component === 'object' &&
      typeof (components.Component as any).renderReqToHTML === 'function'
996 997 998
    const isSSG = !!components.getStaticProps
    const isServerProps = !!components.getServerSideProps
    const hasStaticPaths = !!components.getStaticPaths
999

1000 1001 1002 1003
    if (!query.amp) {
      delete query.amp
    }

1004
    // Toggle whether or not this is a Data request
1005
    const isDataReq = !!query._nextDataReq && (isSSG || isServerProps)
1006 1007
    delete query._nextDataReq

1008 1009 1010 1011 1012 1013 1014 1015
    let previewData: string | false | object | undefined
    let isPreviewMode = false

    if (isServerProps || isSSG) {
      previewData = tryGetPreviewData(req, res, this.renderOpts.previewProps)
      isPreviewMode = previewData !== false
    }

1016 1017 1018 1019 1020 1021
    // Compute the iSSG cache key. We use the rewroteUrl since
    // pages with fallback: false are allowed to be rewritten to
    // and we need to look up the path by the rewritten path
    let urlPathname = (req as any)._nextRewroteUrl
      ? (req as any)._nextRewroteUrl
      : `${parseUrl(req.url || '').pathname!}`
1022

1023 1024 1025
    // remove trailing slash
    urlPathname = urlPathname.replace(/(?!^)\/$/, '')

1026 1027 1028
    // remove /_next/data prefix from urlPathname so it matches
    // for direct page visit and /_next/data visit
    if (isDataReq && urlPathname.includes(this.buildId)) {
1029 1030 1031
      urlPathname = denormalizePagePath(
        (urlPathname.split(this.buildId).pop() || '/').replace(/\.json$/, '')
      )
1032 1033
    }

1034 1035 1036 1037
    const ssgCacheKey =
      isPreviewMode || !isSSG
        ? undefined // Preview mode bypasses the cache
        : `${urlPathname}${query.amp ? '.amp' : ''}`
J
JJ Kasper 已提交
1038 1039

    // Complete the response with cached data if its present
1040 1041 1042
    const cachedData = ssgCacheKey
      ? await this.incrementalCache.get(ssgCacheKey)
      : undefined
1043

J
JJ Kasper 已提交
1044
    if (cachedData) {
1045
      const data = isDataReq
J
JJ Kasper 已提交
1046 1047 1048
        ? JSON.stringify(cachedData.pageData)
        : cachedData.html

1049
      sendPayload(
1050
        req,
J
JJ Kasper 已提交
1051 1052
        res,
        data,
1053
        isDataReq ? 'json' : 'html',
1054 1055 1056 1057
        {
          generateEtags: this.renderOpts.generateEtags,
          poweredByHeader: this.renderOpts.poweredByHeader,
        },
1058 1059 1060 1061 1062 1063 1064 1065 1066
        !this.renderOpts.dev
          ? {
              private: isPreviewMode,
              stateful: false, // GSP response
              revalidate:
                cachedData.curRevalidate !== undefined
                  ? cachedData.curRevalidate
                  : /* default to minimum revalidate (this should be an invariant) */ 1,
            }
1067
          : undefined
J
JJ Kasper 已提交
1068 1069 1070 1071 1072 1073
      )

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

J
JJ Kasper 已提交
1076
    // If we're here, that means data is missing or it's stale.
1077 1078 1079 1080 1081 1082
    const maybeCoalesceInvoke = ssgCacheKey
      ? (fn: any) => withCoalescedInvoke(fn).bind(null, ssgCacheKey, [])
      : (fn: any) => async () => {
          const value = await fn()
          return { isOrigin: true, value }
        }
J
JJ Kasper 已提交
1083

1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099
    const doRender = maybeCoalesceInvoke(
      async (): Promise<{
        html: string | null
        pageData: any
        sprRevalidate: number | false
      }> => {
        let pageData: any
        let html: string | null
        let sprRevalidate: number | false

        let renderResult
        // handle serverless
        if (isLikeServerless) {
          renderResult = await (components.Component as any).renderReqToHTML(
            req,
            res,
P
Prateek Bhatnagar 已提交
1100 1101 1102 1103
            'passthrough',
            {
              fontManifest: this.renderOpts.fontManifest,
            }
1104
          )
J
JJ Kasper 已提交
1105

1106 1107 1108 1109 1110 1111 1112 1113
          html = renderResult.html
          pageData = renderResult.renderOpts.pageData
          sprRevalidate = renderResult.renderOpts.revalidate
        } else {
          const renderOpts: RenderOpts = {
            ...components,
            ...opts,
            isDataReq,
1114 1115 1116 1117 1118
            resolvedUrl: formatUrl({
              pathname: urlPathname,
              // make sure to only add query values from original URL
              query: parseUrl(req.url || '', true).query,
            }),
1119
          }
1120

1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132
          renderResult = await renderToHTML(
            req,
            res,
            pathname,
            query,
            renderOpts
          )

          html = renderResult
          // TODO: change this to a different passing mechanism
          pageData = (renderOpts as any).pageData
          sprRevalidate = (renderOpts as any).revalidate
J
JJ Kasper 已提交
1133 1134
        }

1135
        return { html, pageData, sprRevalidate }
J
JJ Kasper 已提交
1136
      }
1137
    )
J
JJ Kasper 已提交
1138

1139
    const isProduction = !this.renderOpts.dev
J
Joe Haddad 已提交
1140
    const isDynamicPathname = isDynamicRoute(pathname)
1141
    const didRespond = isResSent(res)
1142

1143
    const { staticPaths, fallbackMode } = hasStaticPaths
1144
      ? await this.getStaticPaths(pathname)
1145
      : { staticPaths: undefined, fallbackMode: false }
1146

1147 1148 1149 1150 1151
    // When we did not respond from cache, we need to choose to block on
    // rendering or return a skeleton.
    //
    // * Data requests always block.
    //
1152 1153
    // * Blocking mode fallback always blocks.
    //
1154 1155
    // * Preview mode toggles all pages to be resolved in a blocking manner.
    //
1156
    // * Non-dynamic pages should block (though this is an impossible
1157 1158
    //   case in production).
    //
1159 1160
    // * Dynamic pages should return their skeleton if not defined in
    //   getStaticPaths, then finish the data request on the client-side.
1161
    //
J
Joe Haddad 已提交
1162
    if (
1163
      fallbackMode !== 'blocking' &&
1164
      ssgCacheKey &&
1165 1166 1167
      !didRespond &&
      !isPreviewMode &&
      isDynamicPathname &&
1168 1169 1170
      // Development should trigger fallback when the path is not in
      // `getStaticPaths`
      (isProduction || !staticPaths || !staticPaths.includes(urlPathname))
J
Joe Haddad 已提交
1171
    ) {
1172 1173 1174 1175 1176
      if (
        // In development, fall through to render to handle missing
        // getStaticPaths.
        (isProduction || staticPaths) &&
        // When fallback isn't present, abort this render so we 404
1177
        fallbackMode !== 'static'
1178
      ) {
1179
        throw new NoFallbackError()
1180 1181
      }

1182 1183
      if (!isDataReq) {
        let html: string
1184

1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196
        // Production already emitted the fallback as static HTML.
        if (isProduction) {
          html = await this.incrementalCache.getFallback(pathname)
        }
        // We need to generate the fallback on-demand for development.
        else {
          query.__nextFallback = 'true'
          if (isLikeServerless) {
            prepareServerlessUrl(req, query)
          }
          const { value: renderResult } = await doRender()
          html = renderResult.html
1197 1198
        }

1199 1200 1201 1202 1203 1204
        sendPayload(req, res, html, 'html', {
          generateEtags: this.renderOpts.generateEtags,
          poweredByHeader: this.renderOpts.poweredByHeader,
        })
        return null
      }
1205 1206
    }

1207 1208 1209
    const {
      isOrigin,
      value: { html, pageData, sprRevalidate },
1210
    } = await doRender()
1211 1212
    let resHtml = html
    if (!isResSent(res) && (isSSG || isDataReq || isServerProps)) {
1213
      sendPayload(
1214
        req,
1215 1216
        res,
        isDataReq ? JSON.stringify(pageData) : html,
1217
        isDataReq ? 'json' : 'html',
1218 1219 1220 1221
        {
          generateEtags: this.renderOpts.generateEtags,
          poweredByHeader: this.renderOpts.poweredByHeader,
        },
1222
        !this.renderOpts.dev || (isServerProps && !isDataReq)
1223 1224
          ? {
              private: isPreviewMode,
1225
              stateful: !isSSG,
1226 1227
              revalidate: sprRevalidate,
            }
1228
          : undefined
1229
      )
1230
      resHtml = null
1231
    }
J
JJ Kasper 已提交
1232

1233
    // Update the cache if the head request and cacheable
1234
    if (isOrigin && ssgCacheKey) {
1235 1236 1237 1238 1239
      await this.incrementalCache.set(
        ssgCacheKey,
        { html: html!, pageData },
        sprRevalidate
      )
1240 1241
    }

1242
    return resHtml
1243 1244
  }

1245
  public async renderToHTML(
J
Joe Haddad 已提交
1246 1247 1248
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1249
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1250
  ): Promise<string | null> {
1251 1252 1253
    try {
      const result = await this.findPageComponents(pathname, query)
      if (result) {
1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265
        try {
          return await this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
            result,
            { ...this.renderOpts }
          )
        } catch (err) {
          if (!(err instanceof NoFallbackError)) {
            throw err
          }
1266
        }
1267
      }
J
Joe Haddad 已提交
1268

1269 1270 1271 1272 1273 1274
      if (this.dynamicRoutes) {
        for (const dynamicRoute of this.dynamicRoutes) {
          const params = dynamicRoute.match(pathname)
          if (!params) {
            continue
          }
J
Joe Haddad 已提交
1275

1276
          const dynamicRouteResult = await this.findPageComponents(
1277 1278 1279 1280
            dynamicRoute.page,
            query,
            params
          )
1281
          if (dynamicRouteResult) {
1282 1283 1284 1285 1286
            try {
              return await this.renderToHTMLWithComponents(
                req,
                res,
                dynamicRoute.page,
1287
                dynamicRouteResult,
1288 1289 1290 1291 1292 1293
                { ...this.renderOpts, params }
              )
            } catch (err) {
              if (!(err instanceof NoFallbackError)) {
                throw err
              }
1294
            }
J
Joe Haddad 已提交
1295 1296
          }
        }
1297 1298 1299 1300 1301 1302 1303 1304 1305
      }
    } catch (err) {
      this.logError(err)
      res.statusCode = 500
      return await this.renderErrorToHTML(err, req, res, pathname, query)
    }

    res.statusCode = 404
    return await this.renderErrorToHTML(null, req, res, pathname, query)
N
Naoyuki Kanezawa 已提交
1306 1307
  }

J
Joe Haddad 已提交
1308 1309 1310 1311 1312
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1313
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1314 1315 1316
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
1317
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
1318
    )
N
Naoyuki Kanezawa 已提交
1319
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
1320
    if (html === null) {
1321 1322
      return
    }
1323
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
1324 1325
  }

1326 1327 1328 1329 1330 1331 1332 1333 1334
  private customErrorNo404Warn = execOnce(() => {
    console.warn(
      chalk.bold.yellow(`Warning: `) +
        chalk.yellow(
          `You have added a custom /_error page without a custom /404 page. This prevents the 404 page from being auto statically optimized.\nSee here for info: https://err.sh/next.js/custom-error-no-custom-404`
        )
    )
  })

J
Joe Haddad 已提交
1335 1336 1337 1338 1339
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
1340
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1341
  ) {
1342
    let result: null | FindComponentsResult = null
1343

1344 1345 1346
    const is404 = res.statusCode === 404
    let using404Page = false

1347
    // use static 404 page if available and is 404 response
1348
    if (is404) {
1349 1350
      result = await this.findPageComponents('/404')
      using404Page = result !== null
1351 1352 1353 1354 1355 1356
    }

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

1357 1358 1359
    if (
      process.env.NODE_ENV !== 'production' &&
      !using404Page &&
1360 1361
      (await this.hasPage('/_error')) &&
      !(await this.hasPage('/404'))
1362 1363 1364 1365
    ) {
      this.customErrorNo404Warn()
    }

1366
    let html: string | null
1367
    try {
1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378
      try {
        html = await this.renderToHTMLWithComponents(
          req,
          res,
          using404Page ? '/404' : '/_error',
          result!,
          {
            ...this.renderOpts,
            err,
          }
        )
1379 1380
      } catch (maybeFallbackError) {
        if (maybeFallbackError instanceof NoFallbackError) {
1381
          throw new Error('invariant: failed to render error page')
1382
        }
1383
        throw maybeFallbackError
1384
      }
1385 1386
    } catch (renderToHtmlError) {
      console.error(renderToHtmlError)
1387 1388 1389 1390
      res.statusCode = 500
      html = 'Internal Server Error'
    }
    return html
N
Naoyuki Kanezawa 已提交
1391 1392
  }

J
Joe Haddad 已提交
1393 1394 1395
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
1396
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1397
  ): Promise<void> {
1398 1399
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
N
Naoyuki Kanezawa 已提交
1400
    res.statusCode = 404
1401
    return this.renderError(null, req, res, pathname!, query)
N
Naoyuki Kanezawa 已提交
1402
  }
N
Naoyuki Kanezawa 已提交
1403

J
Joe Haddad 已提交
1404 1405 1406 1407
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
1408
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1409
  ): Promise<void> {
A
Arunoda Susiripala 已提交
1410
    if (!this.isServeableUrl(path)) {
1411
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
1412 1413
    }

1414 1415 1416 1417 1418 1419
    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 已提交
1420
    try {
1421
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
1422
    } catch (err) {
T
Tim Neutkens 已提交
1423
      if (err.code === 'ENOENT' || err.statusCode === 404) {
1424
        this.render404(req, res, parsedUrl)
1425 1426 1427
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
1428 1429 1430 1431 1432 1433
      } else {
        throw err
      }
    }
  }

1434 1435 1436 1437 1438 1439 1440 1441 1442
  private _validFilesystemPathSet: Set<string> | null = null
  private getFilesystemPaths(): Set<string> {
    if (this._validFilesystemPathSet) {
      return this._validFilesystemPathSet
    }

    const pathUserFilesStatic = join(this.dir, 'static')
    let userFilesStatic: string[] = []
    if (this.hasStaticDir && fs.existsSync(pathUserFilesStatic)) {
J
Joe Haddad 已提交
1443
      userFilesStatic = recursiveReadDirSync(pathUserFilesStatic).map((f) =>
1444 1445 1446 1447 1448 1449
        join('.', 'static', f)
      )
    }

    let userFilesPublic: string[] = []
    if (this.publicDir && fs.existsSync(this.publicDir)) {
J
Joe Haddad 已提交
1450
      userFilesPublic = recursiveReadDirSync(this.publicDir).map((f) =>
1451 1452 1453 1454 1455 1456 1457
        join('.', 'public', f)
      )
    }

    let nextFilesStatic: string[] = []
    nextFilesStatic = recursiveReadDirSync(
      join(this.distDir, 'static')
J
Joe Haddad 已提交
1458
    ).map((f) => join('.', relative(this.dir, this.distDir), 'static', f))
1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492

    return (this._validFilesystemPathSet = new Set<string>([
      ...nextFilesStatic,
      ...userFilesPublic,
      ...userFilesStatic,
    ]))
  }

  protected isServeableUrl(untrustedFileUrl: string): boolean {
    // This method mimics what the version of `send` we use does:
    // 1. decodeURIComponent:
    //    https://github.com/pillarjs/send/blob/0.17.1/index.js#L989
    //    https://github.com/pillarjs/send/blob/0.17.1/index.js#L518-L522
    // 2. resolve:
    //    https://github.com/pillarjs/send/blob/de073ed3237ade9ff71c61673a34474b30e5d45b/index.js#L561

    let decodedUntrustedFilePath: string
    try {
      // (1) Decode the URL so we have the proper file name
      decodedUntrustedFilePath = decodeURIComponent(untrustedFileUrl)
    } catch {
      return false
    }

    // (2) Resolve "up paths" to determine real request
    const untrustedFilePath = resolve(decodedUntrustedFilePath)

    // don't allow null bytes anywhere in the file path
    if (untrustedFilePath.indexOf('\0') !== -1) {
      return false
    }

    // Check if .next/static, static and public are in the path.
    // If not the path is not available.
A
Arunoda Susiripala 已提交
1493
    if (
1494 1495 1496
      (untrustedFilePath.startsWith(join(this.distDir, 'static') + sep) ||
        untrustedFilePath.startsWith(join(this.dir, 'static') + sep) ||
        untrustedFilePath.startsWith(join(this.dir, 'public') + sep)) === false
A
Arunoda Susiripala 已提交
1497 1498 1499 1500
    ) {
      return false
    }

1501 1502 1503 1504
    // Check against the real filesystem paths
    const filesystemUrls = this.getFilesystemPaths()
    const resolved = relative(this.dir, untrustedFilePath)
    return filesystemUrls.has(resolved)
A
Arunoda Susiripala 已提交
1505 1506
  }

1507
  protected readBuildId(): string {
1508 1509 1510 1511 1512
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
1513
        throw new Error(
1514
          `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 已提交
1515
        )
1516 1517 1518
      }

      throw err
1519
    }
1520
  }
1521

1522
  protected get _isLikeServerless(): boolean {
1523 1524
    return isTargetLikeServerless(this.nextConfig.target)
  }
1525
}
1526

1527 1528 1529 1530
function prepareServerlessUrl(
  req: IncomingMessage,
  query: ParsedUrlQuery
): void {
1531 1532 1533 1534 1535 1536 1537 1538 1539 1540
  const curUrl = parseUrl(req.url!, true)
  req.url = formatUrl({
    ...curUrl,
    search: undefined,
    query: {
      ...curUrl.query,
      ...query,
    },
  })
}
1541 1542

class NoFallbackError extends Error {}