next-server.ts 50.5 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 8 9 10 11
import {
  parse as parseQs,
  stringify as stringifyQs,
  ParsedUrlQuery,
} from 'querystring'
12
import { format as formatUrl, parse as parseUrl, UrlWithParsedQuery } from 'url'
13
import { PrerenderManifest } from '../../build'
J
Joe Haddad 已提交
14 15 16 17 18 19
import {
  getRedirectStatus,
  Header,
  Redirect,
  Rewrite,
  RouteType,
20 21
  CustomRoutes,
} from '../../lib/load-custom-routes'
J
JJ Kasper 已提交
22
import { withCoalescedInvoke } from '../../lib/coalesced-function'
J
Joe Haddad 已提交
23 24
import {
  BUILD_ID_FILE,
25
  CLIENT_PUBLIC_FILES_PATH,
J
Joe Haddad 已提交
26 27
  CLIENT_STATIC_FILES_PATH,
  CLIENT_STATIC_FILES_RUNTIME,
28
  PAGES_MANIFEST,
J
Joe Haddad 已提交
29
  PHASE_PRODUCTION_SERVER,
J
Joe Haddad 已提交
30
  PRERENDER_MANIFEST,
31
  ROUTES_MANIFEST,
32
  SERVERLESS_DIRECTORY,
J
Joe Haddad 已提交
33
  SERVER_DIRECTORY,
T
Tim Neutkens 已提交
34
} from '../lib/constants'
J
Joe Haddad 已提交
35 36 37 38
import {
  getRouteMatcher,
  getRouteRegex,
  getSortedRoutes,
39
  isDynamicRoute,
J
Joe Haddad 已提交
40
} from '../lib/router/utils'
41
import * as envConfig from '../lib/runtime-config'
J
Joe Haddad 已提交
42
import { isResSent, NextApiRequest, NextApiResponse } from '../lib/utils'
43 44 45 46 47 48 49
import {
  apiResolver,
  setLazyProp,
  getCookieParser,
  tryGetPreviewData,
  __ApiPreviewProps,
} from './api-utils'
50
import loadConfig, { isTargetLikeServerless } from './config'
51
import pathMatch from '../lib/router/utils/path-match'
J
Joe Haddad 已提交
52
import { recursiveReadDirSync } from './lib/recursive-readdir-sync'
53
import { loadComponents, LoadComponentsReturnType } from './load-components'
J
Joe Haddad 已提交
54
import { normalizePagePath } from './normalize-page-path'
55
import { RenderOpts, RenderOptsPartial, renderToHTML } from './render'
P
Prateek Bhatnagar 已提交
56
import { getPagePath, requireFontManifest } from './require'
57 58 59
import Router, {
  DynamicRoutes,
  PageChecker,
J
Joe Haddad 已提交
60 61 62
  Params,
  route,
  Route,
63
} from './router'
64
import prepareDestination from '../lib/router/utils/prepare-destination'
65
import { sendPayload } from './send-payload'
J
Joe Haddad 已提交
66
import { serveStatic } from './serve-static'
67
import { IncrementalCache } from './incremental-cache'
68
import { execOnce } from '../lib/utils'
69
import { isBlockedPage } from './utils'
70
import { compile as compilePathToRegex } from 'next/dist/compiled/path-to-regexp'
71
import { loadEnvConfig } from '@next/env'
72
import './node-polyfill-fetch'
J
Jan Potoms 已提交
73
import { PagesManifest } from '../../build/webpack/plugins/pages-manifest-plugin'
74
import { removePathTrailingSlash } from '../../client/normalize-trailing-slash'
75
import getRouteFromAssetPath from '../lib/router/utils/get-route-from-asset-path'
P
Prateek Bhatnagar 已提交
76
import { FontManifest } from './font-utils'
77
import { denormalizePagePath } from './denormalize-page-path'
78 79 80
import accept from '@hapi/accept'
import { normalizeLocalePath } from '../lib/i18n/normalize-locale-path'
import { detectLocaleCookie } from '../lib/i18n/detect-locale-cookie'
81
import * as Log from '../../build/output/log'
82
import { detectDomainLocale } from '../lib/i18n/detect-domain-locale'
J
JJ Kasper 已提交
83 84

const getCustomRouteMatcher = pathMatch(true)
85 86 87

type NextConfig = any

88 89 90 91 92 93
type Middleware = (
  req: IncomingMessage,
  res: ServerResponse,
  next: (err?: Error) => void
) => void

94 95 96 97 98
type FindComponentsResult = {
  components: LoadComponentsReturnType
  query: ParsedUrlQuery
}

T
Tim Neutkens 已提交
99
export type ServerConstructor = {
100 101 102
  /**
   * Where the Next project is located - @default '.'
   */
J
Joe Haddad 已提交
103
  dir?: string
104 105 106
  /**
   * Hide error messages containing server information - @default false
   */
J
Joe Haddad 已提交
107
  quiet?: boolean
108 109 110
  /**
   * Object what you would use in next.config.js - @default {}
   */
111
  conf?: NextConfig
J
JJ Kasper 已提交
112
  dev?: boolean
113
  customServer?: boolean
114
}
115

N
nkzawa 已提交
116
export default class Server {
117 118 119 120
  dir: string
  quiet: boolean
  nextConfig: NextConfig
  distDir: string
121
  pagesDir?: string
122
  publicDir: string
123
  hasStaticDir: boolean
124
  serverBuildDir: string
J
Jan Potoms 已提交
125
  pagesManifest?: PagesManifest
126 127
  buildId: string
  renderOpts: {
T
Tim Neutkens 已提交
128
    poweredByHeader: boolean
J
Joe Haddad 已提交
129 130 131
    buildId: string
    generateEtags: boolean
    runtimeConfig?: { [key: string]: any }
132 133 134
    assetPrefix?: string
    canonicalBase: string
    dev?: boolean
135
    previewProps: __ApiPreviewProps
136
    customServer?: boolean
137
    ampOptimizerConfig?: { [key: string]: any }
138
    basePath: string
P
Prateek Bhatnagar 已提交
139
    optimizeFonts: boolean
A
Alex Castle 已提交
140
    images: string
P
Prateek Bhatnagar 已提交
141
    fontManifest: FontManifest
142
    optimizeImages: boolean
143 144
    locale?: string
    locales?: string[]
145
    defaultLocale?: string
146
  }
147
  private compression?: Middleware
J
JJ Kasper 已提交
148
  private onErrorMiddleware?: ({ err }: { err: Error }) => Promise<void>
149
  private incrementalCache: IncrementalCache
150
  router: Router
151
  protected dynamicRoutes?: DynamicRoutes
152
  protected customRoutes: CustomRoutes
153

J
Joe Haddad 已提交
154 155 156 157
  public constructor({
    dir = '.',
    quiet = false,
    conf = null,
J
JJ Kasper 已提交
158
    dev = false,
159
    customServer = true,
J
Joe Haddad 已提交
160
  }: ServerConstructor = {}) {
N
nkzawa 已提交
161
    this.dir = resolve(dir)
N
Naoyuki Kanezawa 已提交
162
    this.quiet = quiet
T
Tim Neutkens 已提交
163
    const phase = this.currentPhase()
164
    loadEnvConfig(this.dir, dev, Log)
165

166
    this.nextConfig = loadConfig(phase, this.dir, conf)
167
    this.distDir = join(this.dir, this.nextConfig.distDir)
168
    this.publicDir = join(this.dir, CLIENT_PUBLIC_FILES_PATH)
169
    this.hasStaticDir = fs.existsSync(join(this.dir, 'static'))
T
Tim Neutkens 已提交
170

171 172
    // Only serverRuntimeConfig needs the default
    // publicRuntimeConfig gets it's default in client/index.js
J
Joe Haddad 已提交
173 174 175 176 177
    const {
      serverRuntimeConfig = {},
      publicRuntimeConfig,
      assetPrefix,
      generateEtags,
178
      compress,
J
Joe Haddad 已提交
179
    } = this.nextConfig
180

T
Tim Neutkens 已提交
181
    this.buildId = this.readBuildId()
182

183
    this.renderOpts = {
T
Tim Neutkens 已提交
184
      poweredByHeader: this.nextConfig.poweredByHeader,
185
      canonicalBase: this.nextConfig.amp.canonicalBase,
186
      buildId: this.buildId,
187
      generateEtags,
188
      previewProps: this.getPreviewProps(),
189
      customServer: customServer === true ? true : undefined,
190
      ampOptimizerConfig: this.nextConfig.experimental.amp?.optimizer,
191
      basePath: this.nextConfig.basePath,
A
Alex Castle 已提交
192
      images: JSON.stringify(this.nextConfig.images),
193 194 195 196 197
      optimizeFonts: this.nextConfig.experimental.optimizeFonts && !dev,
      fontManifest:
        this.nextConfig.experimental.optimizeFonts && !dev
          ? requireFontManifest(this.distDir, this._isLikeServerless)
          : null,
198
      optimizeImages: this.nextConfig.experimental.optimizeImages,
199
    }
N
Naoyuki Kanezawa 已提交
200

201 202
    // Only the `publicRuntimeConfig` key is exposed to the client side
    // It'll be rendered as part of __NEXT_DATA__ on the client side
203
    if (Object.keys(publicRuntimeConfig).length > 0) {
204
      this.renderOpts.runtimeConfig = publicRuntimeConfig
205 206
    }

207
    if (compress && this.nextConfig.target === 'server') {
208 209 210
      this.compression = compression() as Middleware
    }

211
    // Initialize next/config with the environment configuration
212 213 214 215
    envConfig.setConfig({
      serverRuntimeConfig,
      publicRuntimeConfig,
    })
216

217 218 219 220 221 222 223 224 225 226
    this.serverBuildDir = join(
      this.distDir,
      this._isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY
    )
    const pagesManifestPath = join(this.serverBuildDir, PAGES_MANIFEST)

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

227
    this.customRoutes = this.getCustomRoutes()
J
JJ Kasper 已提交
228
    this.router = new Router(this.generateRoutes())
229
    this.setAssetPrefix(assetPrefix)
J
JJ Kasper 已提交
230

231 232 233
    // call init-server middleware, this is also handled
    // individually in serverless bundles when deployed
    if (!dev && this.nextConfig.experimental.plugins) {
234 235
      const initServer = require(join(this.serverBuildDir, 'init-server.js'))
        .default
236
      this.onErrorMiddleware = require(join(
237
        this.serverBuildDir,
238 239 240 241 242
        'on-error-server.js'
      )).default
      initServer()
    }

243
    this.incrementalCache = new IncrementalCache({
J
JJ Kasper 已提交
244 245 246 247
      dev,
      distDir: this.distDir,
      pagesDir: join(
        this.distDir,
248
        this._isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY,
J
JJ Kasper 已提交
249 250 251 252
        'pages'
      ),
      flushToDisk: this.nextConfig.experimental.sprFlushToDisk,
    })
P
Prateek Bhatnagar 已提交
253 254 255 256 257 258 259 260 261 262

    /**
     * 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)
    }
263 264 265
    if (this.renderOpts.optimizeImages) {
      process.env.__NEXT_OPTIMIZE_IMAGES = JSON.stringify(true)
    }
N
Naoyuki Kanezawa 已提交
266
  }
N
nkzawa 已提交
267

268
  protected currentPhase(): string {
269
    return PHASE_PRODUCTION_SERVER
270 271
  }

272 273 274 275
  private logError(err: Error): void {
    if (this.onErrorMiddleware) {
      this.onErrorMiddleware({ err })
    }
276
    if (this.quiet) return
277
    console.error(err)
278 279
  }

280
  private async handleRequest(
J
Joe Haddad 已提交
281 282
    req: IncomingMessage,
    res: ServerResponse,
283
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
284
  ): Promise<void> {
285 286
    setLazyProp({ req: req as any }, 'cookies', getCookieParser(req))

287
    // Parse url if parsedUrl not provided
288
    if (!parsedUrl || typeof parsedUrl !== 'object') {
289 290
      const url: any = req.url
      parsedUrl = parseUrl(url, true)
291
    }
292

293 294 295
    // 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 已提交
296
    }
297

298
    const { basePath } = this.nextConfig
299
    const { i18n } = this.nextConfig.experimental
300

301 302 303 304 305
    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 已提交
306 307
    }

308
    if (i18n && !parsedUrl.pathname?.startsWith('/_next')) {
309 310
      // get pathname from URL with basePath stripped for locale detection
      const { pathname, ...parsed } = parseUrl(req.url || '/')
311
      let defaultLocale = i18n.defaultLocale
312 313
      let detectedLocale = detectLocaleCookie(req, i18n.locales)

314 315 316 317 318
      const detectedDomain = detectDomainLocale(i18n.domains, req)
      if (detectedDomain) {
        defaultLocale = detectedDomain.defaultLocale
        detectedLocale = defaultLocale
      }
319

320
      if (!detectedLocale) {
321 322
        detectedLocale = accept.language(
          req.headers['accept-language'],
323
          i18n.locales
324
        )
325 326
      }

327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354
      let localeDomainRedirect: string | undefined
      const localePathResult = normalizeLocalePath(pathname!, i18n.locales)

      if (localePathResult.detectedLocale) {
        detectedLocale = localePathResult.detectedLocale
        req.url = formatUrl({
          ...parsed,
          pathname: localePathResult.pathname,
        })
        parsedUrl.pathname = localePathResult.pathname

        // check if the locale prefix matches a domain's defaultLocale
        // and we're on a locale specific domain if so redirect to that domain
        if (detectedDomain) {
          const matchedDomain = detectDomainLocale(
            i18n.domains,
            undefined,
            detectedLocale
          )

          if (matchedDomain) {
            localeDomainRedirect = `http${matchedDomain.http ? '' : 's'}://${
              matchedDomain?.domain
            }`
          }
        }
      }

355
      const denormalizedPagePath = denormalizePagePath(pathname || '/')
356
      const detectedDefaultLocale =
357 358
        !detectedLocale ||
        detectedLocale.toLowerCase() === defaultLocale.toLowerCase()
359
      const shouldStripDefaultLocale =
360
        detectedDefaultLocale &&
361 362
        denormalizedPagePath.toLowerCase() ===
          `/${i18n.defaultLocale.toLowerCase()}`
363 364
      const shouldAddLocalePrefix =
        !detectedDefaultLocale && denormalizedPagePath === '/'
365

366
      detectedLocale = detectedLocale || i18n.defaultLocale
367

368 369
      if (
        i18n.localeDetection !== false &&
370 371 372
        (localeDomainRedirect ||
          shouldAddLocalePrefix ||
          shouldStripDefaultLocale)
373 374 375 376 377 378
      ) {
        res.setHeader(
          'Location',
          formatUrl({
            // make sure to include any query values when redirecting
            ...parsed,
379 380 381 382 383
            pathname: localeDomainRedirect
              ? localeDomainRedirect
              : shouldStripDefaultLocale
              ? '/'
              : `/${detectedLocale}`,
384 385 386 387
          })
        )
        res.statusCode = 307
        res.end()
388
        return
389
      }
390
      parsedUrl.query.__nextLocale = detectedLocale || defaultLocale
391 392
    }

393
    res.statusCode = 200
394 395 396
    try {
      return await this.run(req, res, parsedUrl)
    } catch (err) {
J
Joe Haddad 已提交
397 398 399
      this.logError(err)
      res.statusCode = 500
      res.end('Internal Server Error')
400
    }
401 402
  }

403
  public getRequestHandler() {
404
    return this.handleRequest.bind(this)
N
nkzawa 已提交
405 406
  }

407
  public setAssetPrefix(prefix?: string): void {
408
    this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
409 410
  }

411
  // Backwards compatibility
412
  public async prepare(): Promise<void> {}
N
nkzawa 已提交
413

T
Tim Neutkens 已提交
414
  // Backwards compatibility
415
  protected async close(): Promise<void> {}
T
Tim Neutkens 已提交
416

417
  protected setImmutableAssetCacheControl(res: ServerResponse): void {
T
Tim Neutkens 已提交
418
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
N
nkzawa 已提交
419 420
  }

421
  protected getCustomRoutes(): CustomRoutes {
J
JJ Kasper 已提交
422 423 424
    return require(join(this.distDir, ROUTES_MANIFEST))
  }

425 426 427 428
  private _cachedPreviewManifest: PrerenderManifest | undefined
  protected getPrerenderManifest(): PrerenderManifest {
    if (this._cachedPreviewManifest) {
      return this._cachedPreviewManifest
J
Joe Haddad 已提交
429
    }
430 431 432 433 434 435
    const manifest = require(join(this.distDir, PRERENDER_MANIFEST))
    return (this._cachedPreviewManifest = manifest)
  }

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

438
  protected generateRoutes(): {
439
    basePath: string
440 441
    headers: Route[]
    rewrites: Route[]
442
    fsRoutes: Route[]
443
    redirects: Route[]
444 445
    catchAllRoute: Route
    pageChecker: PageChecker
446
    useFileSystemPublicRoutes: boolean
447 448
    dynamicRoutes: DynamicRoutes | undefined
  } {
449 450 451
    const publicRoutes = fs.existsSync(this.publicDir)
      ? this.generatePublicRoutes()
      : []
J
JJ Kasper 已提交
452

453
    const staticFilesRoute = this.hasStaticDir
454 455 456 457 458
      ? [
          {
            // 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.
459
            // See more: https://github.com/vercel/next.js/issues/2617
460
            match: route('/static/:path*'),
461
            name: 'static catchall',
462
            fn: async (req, res, params, parsedUrl) => {
463
              const p = join(this.dir, 'static', ...params.path)
464
              await this.serveStatic(req, res, p, parsedUrl)
465 466 467
              return {
                finished: true,
              }
468 469 470 471
            },
          } as Route,
        ]
      : []
472

473
    const fsRoutes: Route[] = [
T
Tim Neutkens 已提交
474
      {
475
        match: route('/_next/static/:path*'),
476 477
        type: 'route',
        name: '_next/static catchall',
478
        fn: async (req, res, params, parsedUrl) => {
479
          // make sure to 404 for /_next/static itself
480 481 482 483 484 485
          if (!params.path) {
            await this.render404(req, res, parsedUrl)
            return {
              finished: true,
            }
          }
486

J
Joe Haddad 已提交
487 488 489
          if (
            params.path[0] === CLIENT_STATIC_FILES_RUNTIME ||
            params.path[0] === 'chunks' ||
490 491
            params.path[0] === 'css' ||
            params.path[0] === 'media' ||
492
            params.path[0] === this.buildId ||
493
            params.path[0] === 'pages' ||
494
            params.path[1] === 'pages'
J
Joe Haddad 已提交
495
          ) {
T
Tim Neutkens 已提交
496
            this.setImmutableAssetCacheControl(res)
497
          }
J
Joe Haddad 已提交
498 499 500
          const p = join(
            this.distDir,
            CLIENT_STATIC_FILES_PATH,
501
            ...(params.path || [])
J
Joe Haddad 已提交
502
          )
503
          await this.serveStatic(req, res, p, parsedUrl)
504 505 506
          return {
            finished: true,
          }
507
        },
508
      },
J
JJ Kasper 已提交
509 510
      {
        match: route('/_next/data/:path*'),
511 512
        type: 'route',
        name: '_next/data catchall',
J
JJ Kasper 已提交
513
        fn: async (req, res, params, _parsedUrl) => {
J
JJ Kasper 已提交
514 515 516
          // 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) {
517 518 519 520
            await this.render404(req, res, _parsedUrl)
            return {
              finished: true,
            }
J
JJ Kasper 已提交
521 522 523 524 525 526
          }
          // 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')) {
527 528 529 530
            await this.render404(req, res, _parsedUrl)
            return {
              finished: true,
            }
J
JJ Kasper 已提交
531 532 533
          }

          // re-create page's pathname
534 535
          let pathname = `/${params.path.join('/')}`

536 537 538 539
          const { i18n } = this.nextConfig.experimental

          if (i18n) {
            const localePathResult = normalizeLocalePath(pathname, i18n.locales)
540 541
            const { defaultLocale } =
              detectDomainLocale(i18n.domains, req) || {}
542
            let detectedLocale = defaultLocale
543 544 545 546 547

            if (localePathResult.detectedLocale) {
              pathname = localePathResult.pathname
              detectedLocale = localePathResult.detectedLocale
            }
548
            _parsedUrl.query.__nextLocale = detectedLocale!
549 550
          }
          pathname = getRouteFromAssetPath(pathname, '.json')
J
JJ Kasper 已提交
551

J
JJ Kasper 已提交
552
          const parsedUrl = parseUrl(pathname, true)
553

J
JJ Kasper 已提交
554 555 556 557
          await this.render(
            req,
            res,
            pathname,
558
            { ..._parsedUrl.query, _nextDataReq: '1' },
J
JJ Kasper 已提交
559 560
            parsedUrl
          )
561 562 563
          return {
            finished: true,
          }
J
JJ Kasper 已提交
564 565
        },
      },
T
Tim Neutkens 已提交
566
      {
567
        match: route('/_next/:path*'),
568 569
        type: 'route',
        name: '_next catchall',
T
Tim Neutkens 已提交
570
        // This path is needed because `render()` does a check for `/_next` and the calls the routing again
571
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
572
          await this.render404(req, res, parsedUrl)
573 574 575
          return {
            finished: true,
          }
L
Lukáš Huvar 已提交
576 577
        },
      },
578 579
      ...publicRoutes,
      ...staticFilesRoute,
T
Tim Neutkens 已提交
580
    ]
581

582 583 584 585 586 587
    const getCustomRouteBasePath = (r: { basePath?: false }) => {
      return r.basePath !== false && this.renderOpts.dev
        ? this.nextConfig.basePath
        : ''
    }

588 589 590 591
    const getCustomRoute = (r: Rewrite | Redirect | Header, type: RouteType) =>
      ({
        ...r,
        type,
592
        match: getCustomRouteMatcher(`${getCustomRouteBasePath(r)}${r.source}`),
593 594 595 596 597 598 599
        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
600
      }
601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621

      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}`
            )
        }
622
      }
623 624 625 626 627 628 629 630 631 632 633 634
      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)
635
    }
636

637 638 639 640 641 642 643 644 645 646 647 648 649 650 651
    // 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)
652
            }
653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670
            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,
671 672 673
            parsedUrl.query,
            false,
            getCustomRouteBasePath(redirectRoute)
674
          )
675 676 677 678 679 680 681 682

          const { query } = parsedDestination
          delete parsedDestination.query

          parsedDestination.search = stringifyQs(query, undefined, undefined, {
            encodeURIComponent: (str: string) => str,
          } as any)

683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704
          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 {
705
        ...rewriteRoute,
706 707 708 709 710 711 712 713 714
        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,
715 716
            true,
            getCustomRouteBasePath(rewriteRoute)
717
          )
718

719 720
          // external rewrite, proxy it
          if (parsedDestination.protocol) {
721 722 723 724 725 726 727 728 729
            const { query } = parsedDestination
            delete parsedDestination.query
            parsedDestination.search = stringifyQs(
              query,
              undefined,
              undefined,
              { encodeURIComponent: (str) => str }
            )

730 731 732 733 734 735 736 737 738 739 740
            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)
            })
741 742 743
            return {
              finished: true,
            }
744 745
          }
          ;(req as any)._nextRewroteUrl = newUrl
746 747
          ;(req as any)._nextDidRewrite =
            (req as any)._nextRewroteUrl !== req.url
748

749 750 751 752 753 754 755 756
          return {
            finished: false,
            pathname: newUrl,
            query: parsedDestination.query,
          }
        },
      } as Route
    })
757 758 759 760 761 762

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

J
Jan Potoms 已提交
768
        // next.js core assumes page path without trailing slash
769
        pathname = removePathTrailingSlash(pathname)
J
Jan Potoms 已提交
770

771
        if (params?.path?.[0] === 'api') {
772 773 774
          const handled = await this.handleApiRequest(
            req as NextApiRequest,
            res as NextApiResponse,
775
            pathname,
776
            query
777 778 779 780 781 782 783
          )
          if (handled) {
            return { finished: true }
          }
        }

        await this.render(req, res, pathname, query, parsedUrl)
784 785 786 787
        return {
          finished: true,
        }
      },
788
    }
789

790
    const { useFileSystemPublicRoutes } = this.nextConfig
J
Joe Haddad 已提交
791

792 793
    if (useFileSystemPublicRoutes) {
      this.dynamicRoutes = this.getDynamicRoutes()
794
    }
N
nkzawa 已提交
795

796
    return {
797
      headers,
798
      fsRoutes,
799 800
      rewrites,
      redirects,
801
      catchAllRoute,
802
      useFileSystemPublicRoutes,
803
      dynamicRoutes: this.dynamicRoutes,
804
      basePath: this.nextConfig.basePath,
805 806
      pageChecker: this.hasPage.bind(this),
    }
T
Tim Neutkens 已提交
807 808
  }

809
  private async getPagePath(pathname: string): Promise<string> {
810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826
    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
  }

827 828 829 830 831
  protected async _beforeCatchAllRender(
    _req: IncomingMessage,
    _res: ServerResponse,
    _params: Params,
    _parsedUrl: UrlWithParsedQuery
832
  ): Promise<boolean> {
833 834 835
    return false
  }

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

L
Lukáš Huvar 已提交
839 840 841 842 843 844
  /**
   * Resolves `API` request, in development builds on demand
   * @param req http request
   * @param res http response
   * @param pathname path of request
   */
J
Joe Haddad 已提交
845
  private async handleApiRequest(
846 847
    req: IncomingMessage,
    res: ServerResponse,
848 849
    pathname: string,
    query: ParsedUrlQuery
850
  ): Promise<boolean> {
851
    let page = pathname
L
Lukáš Huvar 已提交
852
    let params: Params | boolean = false
853
    let pageFound = await this.hasPage(page)
J
JJ Kasper 已提交
854

855
    if (!pageFound && this.dynamicRoutes) {
L
Lukáš Huvar 已提交
856 857
      for (const dynamicRoute of this.dynamicRoutes) {
        params = dynamicRoute.match(pathname)
858
        if (dynamicRoute.page.startsWith('/api') && params) {
859 860
          page = dynamicRoute.page
          pageFound = true
L
Lukáš Huvar 已提交
861 862 863 864 865
          break
        }
      }
    }

866
    if (!pageFound) {
867
      return false
J
JJ Kasper 已提交
868
    }
869 870 871 872
    // Make sure the page is built before getting the path
    // or else it won't be in the manifest yet
    await this.ensureApiPage(page)

873 874 875 876 877 878 879 880 881 882
    let builtPagePath
    try {
      builtPagePath = await this.getPagePath(page)
    } catch (err) {
      if (err.code === 'ENOENT') {
        return false
      }
      throw err
    }

883
    const pageModule = await require(builtPagePath)
884
    query = { ...query, ...params }
J
JJ Kasper 已提交
885

886
    if (!this.renderOpts.dev && this._isLikeServerless) {
887
      if (typeof pageModule.default === 'function') {
888
        prepareServerlessUrl(req, query)
889 890
        await pageModule.default(req, res)
        return true
J
JJ Kasper 已提交
891 892 893
      }
    }

J
Joe Haddad 已提交
894 895 896 897 898
    await apiResolver(
      req,
      res,
      query,
      pageModule,
899
      this.renderOpts.previewProps,
900
      false,
J
Joe Haddad 已提交
901 902
      this.onErrorMiddleware
    )
903
    return true
L
Lukáš Huvar 已提交
904 905
  }

906
  protected generatePublicRoutes(): Route[] {
907
    const publicFiles = new Set(
908 909 910
      recursiveReadDirSync(this.publicDir).map((p) =>
        encodeURI(p.replace(/\\/g, '/'))
      )
911 912 913 914 915 916 917
    )

    return [
      {
        match: route('/:path*'),
        name: 'public folder catchall',
        fn: async (req, res, params, parsedUrl) => {
918
          const pathParts: string[] = params.path || []
919 920 921 922 923 924 925 926
          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()
          }

927
          const path = `/${pathParts.join('/')}`
928 929 930 931 932

          if (publicFiles.has(path)) {
            await this.serveStatic(
              req,
              res,
933
              join(this.publicDir, ...pathParts),
934 935
              parsedUrl
            )
936 937 938
            return {
              finished: true,
            }
939 940 941 942 943 944 945
          }
          return {
            finished: false,
          }
        },
      } as Route,
    ]
946 947
  }

948
  protected getDynamicRoutes() {
949 950
    return getSortedRoutes(Object.keys(this.pagesManifest!))
      .filter(isDynamicRoute)
J
Joe Haddad 已提交
951
      .map((page) => ({
952 953 954
        page,
        match: getRouteMatcher(getRouteRegex(page)),
      }))
J
Joe Haddad 已提交
955 956
  }

957
  private handleCompression(req: IncomingMessage, res: ServerResponse): void {
958 959 960 961 962
    if (this.compression) {
      this.compression(req, res, () => {})
    }
  }

963
  protected async run(
J
Joe Haddad 已提交
964 965
    req: IncomingMessage,
    res: ServerResponse,
966
    parsedUrl: UrlWithParsedQuery
967
  ): Promise<void> {
968 969
    this.handleCompression(req, res)

970
    try {
971 972
      const matched = await this.router.execute(req, res, parsedUrl)
      if (matched) {
973 974 975 976 977 978 979 980
        return
      }
    } catch (err) {
      if (err.code === 'DECODE_FAILED') {
        res.statusCode = 400
        return this.renderError(null, req, res, '/_error', {})
      }
      throw err
981 982
    }

983
    await this.render404(req, res, parsedUrl)
N
nkzawa 已提交
984 985
  }

986
  protected async sendHTML(
J
Joe Haddad 已提交
987 988
    req: IncomingMessage,
    res: ServerResponse,
989
    html: string
990
  ): Promise<void> {
T
Tim Neutkens 已提交
991
    const { generateEtags, poweredByHeader } = this.renderOpts
992 993 994 995
    return sendPayload(req, res, html, 'html', {
      generateEtags,
      poweredByHeader,
    })
996 997
  }

J
Joe Haddad 已提交
998 999 1000 1001 1002
  public async render(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
1003
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1004
  ): Promise<void> {
1005 1006 1007 1008 1009 1010
    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`
      )
    }

1011 1012 1013 1014 1015 1016 1017 1018 1019 1020
    if (
      this.renderOpts.customServer &&
      pathname === '/index' &&
      !(await this.hasPage('/index'))
    ) {
      // maintain backwards compatibility for custom server
      // (see custom-server integration tests)
      pathname = '/'
    }

1021
    const url: any = req.url
1022

1023 1024 1025 1026
    // 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
1027
    if (
1028 1029 1030
      !query._nextDataReq &&
      (url.match(/^\/_next\//) ||
        (this.hasStaticDir && url.match(/^\/static\//)))
1031
    ) {
1032 1033 1034
      return this.handleRequest(req, res, parsedUrl)
    }

1035
    if (isBlockedPage(pathname)) {
1036
      return this.render404(req, res, parsedUrl)
1037 1038
    }

1039
    const html = await this.renderToHTML(req, res, pathname, query)
1040 1041
    // Request was ended by the user
    if (html === null) {
1042 1043 1044
      return
    }

1045
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
1046
  }
N
nkzawa 已提交
1047

J
Joe Haddad 已提交
1048
  private async findPageComponents(
J
Joe Haddad 已提交
1049
    pathname: string,
1050 1051 1052
    query: ParsedUrlQuery = {},
    params: Params | null = null
  ): Promise<FindComponentsResult | null> {
1053
    let paths = [
1054 1055 1056 1057
      // try serving a static AMP version first
      query.amp ? normalizePagePath(pathname) + '.amp' : null,
      pathname,
    ].filter(Boolean)
1058 1059 1060 1061 1062 1063 1064 1065 1066 1067

    if (query.__nextLocale) {
      paths = [
        ...paths.map(
          (path) => `/${query.__nextLocale}${path === '/' ? '' : path}`
        ),
        ...paths,
      ]
    }

1068
    for (const pagePath of paths) {
J
JJ Kasper 已提交
1069
      try {
1070
        const components = await loadComponents(
J
Joe Haddad 已提交
1071
          this.distDir,
1072 1073
          pagePath!,
          !this.renderOpts.dev && this._isLikeServerless
J
Joe Haddad 已提交
1074
        )
1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086
        // if loading an static HTML file the locale is required
        // to be present since all HTML files are output under their locale
        if (
          query.__nextLocale &&
          typeof components.Component === 'string' &&
          !pagePath?.startsWith(`/${query.__nextLocale}`)
        ) {
          const err = new Error('NOT_FOUND')
          ;(err as any).code = 'ENOENT'
          throw err
        }

1087 1088 1089
        return {
          components,
          query: {
1090
            ...(components.getStaticProps
1091 1092 1093 1094 1095
              ? {
                  amp: query.amp,
                  _nextDataReq: query._nextDataReq,
                  __nextLocale: query.__nextLocale,
                }
1096 1097 1098 1099
              : query),
            ...(params || {}),
          },
        }
J
JJ Kasper 已提交
1100 1101 1102 1103
      } catch (err) {
        if (err.code !== 'ENOENT') throw err
      }
    }
1104
    return null
J
Joe Haddad 已提交
1105 1106
  }

1107
  protected async getStaticPaths(
1108 1109 1110
    pathname: string
  ): Promise<{
    staticPaths: string[] | undefined
1111
    fallbackMode: 'static' | 'blocking' | false
1112
  }> {
1113 1114 1115 1116 1117
    // `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.
1118 1119
    const fallbackField = this.getPrerenderManifest().dynamicRoutes[pathname]
      .fallback
1120

1121 1122 1123 1124 1125 1126 1127 1128 1129
    return {
      staticPaths,
      fallbackMode:
        typeof fallbackField === 'string'
          ? 'static'
          : fallbackField === null
          ? 'blocking'
          : false,
    }
1130 1131
  }

J
Joe Haddad 已提交
1132 1133 1134 1135
  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1136
    { components, query }: FindComponentsResult,
1137
    opts: RenderOptsPartial
1138
  ): Promise<string | null> {
1139
    // we need to ensure the status code if /404 is visited directly
1140
    if (pathname === '/404') {
1141 1142 1143
      res.statusCode = 404
    }

J
JJ Kasper 已提交
1144
    // handle static page
1145 1146
    if (typeof components.Component === 'string') {
      return components.Component
J
Joe Haddad 已提交
1147 1148
    }

J
JJ Kasper 已提交
1149 1150
    // check request state
    const isLikeServerless =
1151 1152
      typeof components.Component === 'object' &&
      typeof (components.Component as any).renderReqToHTML === 'function'
1153 1154 1155
    const isSSG = !!components.getStaticProps
    const isServerProps = !!components.getServerSideProps
    const hasStaticPaths = !!components.getStaticPaths
1156

1157 1158 1159 1160
    if (!query.amp) {
      delete query.amp
    }

1161
    // Toggle whether or not this is a Data request
1162
    const isDataReq = !!query._nextDataReq && (isSSG || isServerProps)
1163 1164
    delete query._nextDataReq

J
JJ Kasper 已提交
1165 1166
    const locale = query.__nextLocale as string
    delete query.__nextLocale
1167 1168 1169

    const { i18n } = this.nextConfig.experimental
    const locales = i18n.locales as string[]
J
JJ Kasper 已提交
1170

1171 1172 1173 1174 1175 1176 1177 1178
    let previewData: string | false | object | undefined
    let isPreviewMode = false

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

1179 1180 1181
    // 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
1182 1183 1184
    let urlPathname = parseUrl(req.url || '').pathname || '/'

    let resolvedUrlPathname = (req as any)._nextRewroteUrl
1185
      ? (req as any)._nextRewroteUrl
1186
      : urlPathname
1187

1188 1189 1190 1191 1192 1193 1194 1195 1196
    resolvedUrlPathname = removePathTrailingSlash(resolvedUrlPathname)
    urlPathname = removePathTrailingSlash(urlPathname)

    const stripNextDataPath = (path: string) => {
      if (path.includes(this.buildId)) {
        path = denormalizePagePath(
          (path.split(this.buildId).pop() || '/').replace(/\.json$/, '')
        )
      }
1197 1198

      if (this.nextConfig.experimental.i18n) {
J
JJ Kasper 已提交
1199
        return normalizeLocalePath(path, locales).pathname
1200
      }
1201 1202
      return path
    }
1203

1204 1205
    // remove /_next/data prefix from urlPathname so it matches
    // for direct page visit and /_next/data visit
1206 1207 1208
    if (isDataReq) {
      resolvedUrlPathname = stripNextDataPath(resolvedUrlPathname)
      urlPathname = stripNextDataPath(urlPathname)
1209 1210
    }

1211 1212 1213
    const ssgCacheKey =
      isPreviewMode || !isSSG
        ? undefined // Preview mode bypasses the cache
1214 1215 1216
        : `${locale ? `/${locale}` : ''}${resolvedUrlPathname}${
            query.amp ? '.amp' : ''
          }`
J
JJ Kasper 已提交
1217 1218

    // Complete the response with cached data if its present
1219 1220 1221
    const cachedData = ssgCacheKey
      ? await this.incrementalCache.get(ssgCacheKey)
      : undefined
1222

J
JJ Kasper 已提交
1223
    if (cachedData) {
1224
      const data = isDataReq
J
JJ Kasper 已提交
1225 1226 1227
        ? JSON.stringify(cachedData.pageData)
        : cachedData.html

1228
      sendPayload(
1229
        req,
J
JJ Kasper 已提交
1230 1231
        res,
        data,
1232
        isDataReq ? 'json' : 'html',
1233 1234 1235 1236
        {
          generateEtags: this.renderOpts.generateEtags,
          poweredByHeader: this.renderOpts.poweredByHeader,
        },
1237 1238 1239 1240 1241 1242 1243 1244 1245
        !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,
            }
1246
          : undefined
J
JJ Kasper 已提交
1247 1248 1249 1250 1251 1252
      )

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

J
JJ Kasper 已提交
1255
    // If we're here, that means data is missing or it's stale.
1256 1257 1258 1259 1260 1261
    const maybeCoalesceInvoke = ssgCacheKey
      ? (fn: any) => withCoalescedInvoke(fn).bind(null, ssgCacheKey, [])
      : (fn: any) => async () => {
          const value = await fn()
          return { isOrigin: true, value }
        }
J
JJ Kasper 已提交
1262

1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278
    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 已提交
1279 1280 1281
            'passthrough',
            {
              fontManifest: this.renderOpts.fontManifest,
1282
              locale,
1283 1284
              locales,
              // defaultLocale,
P
Prateek Bhatnagar 已提交
1285
            }
1286
          )
J
JJ Kasper 已提交
1287

1288 1289 1290 1291
          html = renderResult.html
          pageData = renderResult.renderOpts.pageData
          sprRevalidate = renderResult.renderOpts.revalidate
        } else {
1292 1293 1294 1295 1296 1297 1298
          const origQuery = parseUrl(req.url || '', true).query
          const resolvedUrl = formatUrl({
            pathname: resolvedUrlPathname,
            // make sure to only add query values from original URL
            query: origQuery,
          })

1299 1300 1301 1302
          const renderOpts: RenderOpts = {
            ...components,
            ...opts,
            isDataReq,
1303
            resolvedUrl,
1304
            locale,
1305 1306
            locales,
            // defaultLocale,
1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317
            // For getServerSideProps we need to ensure we use the original URL
            // and not the resolved URL to prevent a hydration mismatch on
            // asPath
            resolvedAsPath: isServerProps
              ? formatUrl({
                  // we use the original URL pathname less the _next/data prefix if
                  // present
                  pathname: urlPathname,
                  query: origQuery,
                })
              : resolvedUrl,
1318
          }
1319

1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331
          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 已提交
1332 1333
        }

1334
        return { html, pageData, sprRevalidate }
J
JJ Kasper 已提交
1335
      }
1336
    )
J
JJ Kasper 已提交
1337

1338
    const isProduction = !this.renderOpts.dev
J
Joe Haddad 已提交
1339
    const isDynamicPathname = isDynamicRoute(pathname)
1340
    const didRespond = isResSent(res)
1341

1342
    const { staticPaths, fallbackMode } = hasStaticPaths
1343
      ? await this.getStaticPaths(pathname)
1344
      : { staticPaths: undefined, fallbackMode: false }
1345

1346 1347 1348 1349 1350
    // When we did not respond from cache, we need to choose to block on
    // rendering or return a skeleton.
    //
    // * Data requests always block.
    //
1351 1352
    // * Blocking mode fallback always blocks.
    //
1353 1354
    // * Preview mode toggles all pages to be resolved in a blocking manner.
    //
1355
    // * Non-dynamic pages should block (though this is an impossible
1356 1357
    //   case in production).
    //
1358 1359
    // * Dynamic pages should return their skeleton if not defined in
    //   getStaticPaths, then finish the data request on the client-side.
1360
    //
J
Joe Haddad 已提交
1361
    if (
1362
      fallbackMode !== 'blocking' &&
1363
      ssgCacheKey &&
1364 1365 1366
      !didRespond &&
      !isPreviewMode &&
      isDynamicPathname &&
1367 1368
      // Development should trigger fallback when the path is not in
      // `getStaticPaths`
1369 1370
      (isProduction ||
        !staticPaths ||
1371 1372 1373 1374 1375
        // static paths always includes locale so make sure it's prefixed
        // with it
        !staticPaths.includes(
          `${locale ? '/' + locale : ''}${resolvedUrlPathname}`
        ))
J
Joe Haddad 已提交
1376
    ) {
1377 1378 1379 1380 1381
      if (
        // In development, fall through to render to handle missing
        // getStaticPaths.
        (isProduction || staticPaths) &&
        // When fallback isn't present, abort this render so we 404
1382
        fallbackMode !== 'static'
1383
      ) {
1384
        throw new NoFallbackError()
1385 1386
      }

1387 1388
      if (!isDataReq) {
        let html: string
1389

1390 1391
        // Production already emitted the fallback as static HTML.
        if (isProduction) {
1392 1393 1394
          html = await this.incrementalCache.getFallback(
            locale ? `/${locale}${pathname}` : pathname
          )
1395 1396 1397 1398 1399 1400 1401 1402 1403
        }
        // 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
1404 1405
        }

1406 1407 1408 1409 1410 1411
        sendPayload(req, res, html, 'html', {
          generateEtags: this.renderOpts.generateEtags,
          poweredByHeader: this.renderOpts.poweredByHeader,
        })
        return null
      }
1412 1413
    }

1414 1415 1416
    const {
      isOrigin,
      value: { html, pageData, sprRevalidate },
1417
    } = await doRender()
1418 1419
    let resHtml = html
    if (!isResSent(res) && (isSSG || isDataReq || isServerProps)) {
1420
      sendPayload(
1421
        req,
1422 1423
        res,
        isDataReq ? JSON.stringify(pageData) : html,
1424
        isDataReq ? 'json' : 'html',
1425 1426 1427 1428
        {
          generateEtags: this.renderOpts.generateEtags,
          poweredByHeader: this.renderOpts.poweredByHeader,
        },
1429
        !this.renderOpts.dev || (isServerProps && !isDataReq)
1430 1431
          ? {
              private: isPreviewMode,
1432
              stateful: !isSSG,
1433 1434
              revalidate: sprRevalidate,
            }
1435
          : undefined
1436
      )
1437
      resHtml = null
1438
    }
J
JJ Kasper 已提交
1439

1440
    // Update the cache if the head request and cacheable
1441
    if (isOrigin && ssgCacheKey) {
1442 1443 1444 1445 1446
      await this.incrementalCache.set(
        ssgCacheKey,
        { html: html!, pageData },
        sprRevalidate
      )
1447 1448
    }

1449
    return resHtml
1450 1451
  }

1452
  public async renderToHTML(
J
Joe Haddad 已提交
1453 1454 1455
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1456
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1457
  ): Promise<string | null> {
1458 1459 1460
    try {
      const result = await this.findPageComponents(pathname, query)
      if (result) {
1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472
        try {
          return await this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
            result,
            { ...this.renderOpts }
          )
        } catch (err) {
          if (!(err instanceof NoFallbackError)) {
            throw err
          }
1473
        }
1474
      }
J
Joe Haddad 已提交
1475

1476 1477 1478 1479 1480 1481
      if (this.dynamicRoutes) {
        for (const dynamicRoute of this.dynamicRoutes) {
          const params = dynamicRoute.match(pathname)
          if (!params) {
            continue
          }
J
Joe Haddad 已提交
1482

1483
          const dynamicRouteResult = await this.findPageComponents(
1484 1485 1486 1487
            dynamicRoute.page,
            query,
            params
          )
1488
          if (dynamicRouteResult) {
1489 1490 1491 1492 1493
            try {
              return await this.renderToHTMLWithComponents(
                req,
                res,
                dynamicRoute.page,
1494
                dynamicRouteResult,
1495 1496 1497 1498 1499 1500
                { ...this.renderOpts, params }
              )
            } catch (err) {
              if (!(err instanceof NoFallbackError)) {
                throw err
              }
1501
            }
J
Joe Haddad 已提交
1502 1503
          }
        }
1504 1505 1506
      }
    } catch (err) {
      this.logError(err)
1507 1508 1509 1510 1511

      if (err && err.code === 'DECODE_FAILED') {
        res.statusCode = 400
        return await this.renderErrorToHTML(err, req, res, pathname, query)
      }
1512 1513 1514 1515 1516
      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 已提交
1517 1518
  }

J
Joe Haddad 已提交
1519 1520 1521 1522 1523
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1524
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1525 1526 1527
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
1528
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
1529
    )
N
Naoyuki Kanezawa 已提交
1530
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
1531
    if (html === null) {
1532 1533
      return
    }
1534
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
1535 1536
  }

1537 1538 1539 1540 1541 1542 1543 1544 1545
  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 已提交
1546 1547 1548 1549 1550
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
1551
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1552
  ) {
1553
    let result: null | FindComponentsResult = null
1554

1555 1556 1557
    const is404 = res.statusCode === 404
    let using404Page = false

1558
    // use static 404 page if available and is 404 response
1559
    if (is404) {
1560
      result = await this.findPageComponents('/404', query)
1561
      using404Page = result !== null
1562 1563 1564 1565 1566 1567
    }

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

1568 1569 1570
    if (
      process.env.NODE_ENV !== 'production' &&
      !using404Page &&
1571 1572
      (await this.hasPage('/_error')) &&
      !(await this.hasPage('/404'))
1573 1574 1575 1576
    ) {
      this.customErrorNo404Warn()
    }

1577
    let html: string | null
1578
    try {
1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589
      try {
        html = await this.renderToHTMLWithComponents(
          req,
          res,
          using404Page ? '/404' : '/_error',
          result!,
          {
            ...this.renderOpts,
            err,
          }
        )
1590 1591
      } catch (maybeFallbackError) {
        if (maybeFallbackError instanceof NoFallbackError) {
1592
          throw new Error('invariant: failed to render error page')
1593
        }
1594
        throw maybeFallbackError
1595
      }
1596 1597
    } catch (renderToHtmlError) {
      console.error(renderToHtmlError)
1598 1599 1600 1601
      res.statusCode = 500
      html = 'Internal Server Error'
    }
    return html
N
Naoyuki Kanezawa 已提交
1602 1603
  }

J
Joe Haddad 已提交
1604 1605 1606
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
1607
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1608
  ): Promise<void> {
1609 1610
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
N
Naoyuki Kanezawa 已提交
1611
    res.statusCode = 404
1612
    return this.renderError(null, req, res, pathname!, query)
N
Naoyuki Kanezawa 已提交
1613
  }
N
Naoyuki Kanezawa 已提交
1614

J
Joe Haddad 已提交
1615 1616 1617 1618
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
1619
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1620
  ): Promise<void> {
A
Arunoda Susiripala 已提交
1621
    if (!this.isServeableUrl(path)) {
1622
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
1623 1624
    }

1625 1626 1627 1628 1629 1630
    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 已提交
1631
    try {
1632
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
1633
    } catch (err) {
T
Tim Neutkens 已提交
1634
      if (err.code === 'ENOENT' || err.statusCode === 404) {
1635
        this.render404(req, res, parsedUrl)
1636 1637 1638
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
1639 1640 1641 1642 1643 1644
      } else {
        throw err
      }
    }
  }

1645 1646 1647 1648 1649 1650 1651 1652 1653
  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 已提交
1654
      userFilesStatic = recursiveReadDirSync(pathUserFilesStatic).map((f) =>
1655 1656 1657 1658 1659 1660
        join('.', 'static', f)
      )
    }

    let userFilesPublic: string[] = []
    if (this.publicDir && fs.existsSync(this.publicDir)) {
J
Joe Haddad 已提交
1661
      userFilesPublic = recursiveReadDirSync(this.publicDir).map((f) =>
1662 1663 1664 1665 1666 1667 1668
        join('.', 'public', f)
      )
    }

    let nextFilesStatic: string[] = []
    nextFilesStatic = recursiveReadDirSync(
      join(this.distDir, 'static')
J
Joe Haddad 已提交
1669
    ).map((f) => join('.', relative(this.dir, this.distDir), 'static', f))
1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703

    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 已提交
1704
    if (
1705 1706 1707
      (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 已提交
1708 1709 1710 1711
    ) {
      return false
    }

1712 1713 1714 1715
    // Check against the real filesystem paths
    const filesystemUrls = this.getFilesystemPaths()
    const resolved = relative(this.dir, untrustedFilePath)
    return filesystemUrls.has(resolved)
A
Arunoda Susiripala 已提交
1716 1717
  }

1718
  protected readBuildId(): string {
1719 1720 1721 1722 1723
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
1724
        throw new Error(
1725
          `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 已提交
1726
        )
1727 1728 1729
      }

      throw err
1730
    }
1731
  }
1732

1733
  protected get _isLikeServerless(): boolean {
1734 1735
    return isTargetLikeServerless(this.nextConfig.target)
  }
1736
}
1737

1738 1739 1740 1741
function prepareServerlessUrl(
  req: IncomingMessage,
  query: ParsedUrlQuery
): void {
1742 1743 1744 1745 1746 1747 1748 1749 1750 1751
  const curUrl = parseUrl(req.url!, true)
  req.url = formatUrl({
    ...curUrl,
    search: undefined,
    query: {
      ...curUrl.query,
      ...query,
    },
  })
}
1752 1753

class NoFallbackError extends Error {}