next-server.ts 52.9 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'
S
Steven 已提交
82
import { imageOptimizer } from './image-optimizer'
83
import { detectDomainLocale } from '../lib/i18n/detect-domain-locale'
84
import cookie from 'next/dist/compiled/cookie'
J
JJ Kasper 已提交
85 86

const getCustomRouteMatcher = pathMatch(true)
87 88 89

type NextConfig = any

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

96 97 98 99 100
type FindComponentsResult = {
  components: LoadComponentsReturnType
  query: ParsedUrlQuery
}

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

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

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

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

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

T
Tim Neutkens 已提交
183
    this.buildId = this.readBuildId()
184

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

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

210
    if (compress && this.nextConfig.target === 'server') {
211 212 213
      this.compression = compression() as Middleware
    }

214
    // Initialize next/config with the environment configuration
215 216 217 218
    envConfig.setConfig({
      serverRuntimeConfig,
      publicRuntimeConfig,
    })
219

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

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

230
    this.customRoutes = this.getCustomRoutes()
J
JJ Kasper 已提交
231
    this.router = new Router(this.generateRoutes())
232
    this.setAssetPrefix(assetPrefix)
J
JJ Kasper 已提交
233

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

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

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

271
  protected currentPhase(): string {
272
    return PHASE_PRODUCTION_SERVER
273 274
  }

S
Steven 已提交
275
  public logError(err: Error): void {
276 277 278
    if (this.onErrorMiddleware) {
      this.onErrorMiddleware({ err })
    }
279
    if (this.quiet) return
280
    console.error(err)
281 282
  }

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

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

296 297 298
    // 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 已提交
299
    }
300

J
Joe Haddad 已提交
301
    const { basePath, i18n } = this.nextConfig
302

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

310
    if (i18n && !parsedUrl.pathname?.startsWith('/_next')) {
311 312
      // get pathname from URL with basePath stripped for locale detection
      const { pathname, ...parsed } = parseUrl(req.url || '/')
313
      let defaultLocale = i18n.defaultLocale
314
      let detectedLocale = detectLocaleCookie(req, i18n.locales)
315 316 317 318
      let acceptPreferredLocale = accept.language(
        req.headers['accept-language'],
        i18n.locales
      )
319

320 321 322 323 324
      const { host } = req?.headers || {}
      // remove port from host and remove port if present
      const hostname = host?.split(':')[0].toLowerCase()

      const detectedDomain = detectDomainLocale(i18n.domains, hostname)
325 326 327 328
      if (detectedDomain) {
        defaultLocale = detectedDomain.defaultLocale
        detectedLocale = defaultLocale
      }
329

330 331
      // if not domain specific locale use accept-language preferred
      detectedLocale = detectedLocale || acceptPreferredLocale
332

333 334 335 336 337 338 339 340 341
      let localeDomainRedirect: string | undefined
      const localePathResult = normalizeLocalePath(pathname!, i18n.locales)

      if (localePathResult.detectedLocale) {
        detectedLocale = localePathResult.detectedLocale
        req.url = formatUrl({
          ...parsed,
          pathname: localePathResult.pathname,
        })
J
JJ Kasper 已提交
342
        ;(req as any).__nextStrippedLocale = true
343
        parsedUrl.pathname = localePathResult.pathname
344 345 346 347 348 349 350 351 352 353
      }

      // If a detected locale is a domain specific locale and we aren't already
      // on that domain and path prefix redirect to it to prevent duplicate
      // content from multiple domains
      if (detectedDomain && parsedUrl.pathname === '/') {
        const localeToCheck = acceptPreferredLocale
        // const localeToCheck = localePathResult.detectedLocale
        //   ? detectedLocale
        //   : acceptPreferredLocale
354

355 356 357
        const matchedDomain = detectDomainLocale(
          i18n.domains,
          undefined,
358
          localeToCheck
359 360
        )

361 362 363 364 365
        if (
          matchedDomain &&
          (matchedDomain.domain !== detectedDomain.domain ||
            localeToCheck !== matchedDomain.defaultLocale)
        ) {
366 367
          localeDomainRedirect = `http${matchedDomain.http ? '' : 's'}://${
            matchedDomain.domain
368 369
          }/${
            localeToCheck === matchedDomain.defaultLocale ? '' : localeToCheck
370
          }`
371 372 373
        }
      }

374
      const denormalizedPagePath = denormalizePagePath(pathname || '/')
375
      const detectedDefaultLocale =
376 377
        !detectedLocale ||
        detectedLocale.toLowerCase() === defaultLocale.toLowerCase()
378 379 380 381
      const shouldStripDefaultLocale = false
      // detectedDefaultLocale &&
      // denormalizedPagePath.toLowerCase() ===
      //   `/${i18n.defaultLocale.toLowerCase()}`
382

383 384
      const shouldAddLocalePrefix =
        !detectedDefaultLocale && denormalizedPagePath === '/'
385

386
      detectedLocale = detectedLocale || i18n.defaultLocale
387

388 389
      if (
        i18n.localeDetection !== false &&
390 391 392
        (localeDomainRedirect ||
          shouldAddLocalePrefix ||
          shouldStripDefaultLocale)
393
      ) {
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415
        // set the NEXT_LOCALE cookie when a user visits the default locale
        // with the locale prefix so that they aren't redirected back to
        // their accept-language preferred locale
        if (
          shouldStripDefaultLocale &&
          acceptPreferredLocale !== defaultLocale
        ) {
          const previous = res.getHeader('set-cookie')

          res.setHeader('set-cookie', [
            ...(typeof previous === 'string'
              ? [previous]
              : Array.isArray(previous)
              ? previous
              : []),
            cookie.serialize('NEXT_LOCALE', defaultLocale, {
              httpOnly: true,
              path: '/',
            }),
          ])
        }

416 417 418 419 420
        res.setHeader(
          'Location',
          formatUrl({
            // make sure to include any query values when redirecting
            ...parsed,
421 422 423 424 425
            pathname: localeDomainRedirect
              ? localeDomainRedirect
              : shouldStripDefaultLocale
              ? '/'
              : `/${detectedLocale}`,
426 427 428 429
          })
        )
        res.statusCode = 307
        res.end()
430
        return
431
      }
432
      parsedUrl.query.__nextLocale = detectedLocale || defaultLocale
433 434
    }

435
    res.statusCode = 200
436 437 438
    try {
      return await this.run(req, res, parsedUrl)
    } catch (err) {
J
Joe Haddad 已提交
439 440 441
      this.logError(err)
      res.statusCode = 500
      res.end('Internal Server Error')
442
    }
443 444
  }

445
  public getRequestHandler() {
446
    return this.handleRequest.bind(this)
N
nkzawa 已提交
447 448
  }

449
  public setAssetPrefix(prefix?: string): void {
450
    this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
451 452
  }

453
  // Backwards compatibility
454
  public async prepare(): Promise<void> {}
N
nkzawa 已提交
455

T
Tim Neutkens 已提交
456
  // Backwards compatibility
457
  protected async close(): Promise<void> {}
T
Tim Neutkens 已提交
458

459
  protected setImmutableAssetCacheControl(res: ServerResponse): void {
T
Tim Neutkens 已提交
460
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
N
nkzawa 已提交
461 462
  }

463
  protected getCustomRoutes(): CustomRoutes {
J
JJ Kasper 已提交
464 465 466
    return require(join(this.distDir, ROUTES_MANIFEST))
  }

467 468 469 470
  private _cachedPreviewManifest: PrerenderManifest | undefined
  protected getPrerenderManifest(): PrerenderManifest {
    if (this._cachedPreviewManifest) {
      return this._cachedPreviewManifest
J
Joe Haddad 已提交
471
    }
472 473 474 475 476 477
    const manifest = require(join(this.distDir, PRERENDER_MANIFEST))
    return (this._cachedPreviewManifest = manifest)
  }

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

480
  protected generateRoutes(): {
481
    basePath: string
482 483
    headers: Route[]
    rewrites: Route[]
484
    fsRoutes: Route[]
485
    redirects: Route[]
486 487
    catchAllRoute: Route
    pageChecker: PageChecker
488
    useFileSystemPublicRoutes: boolean
489 490
    dynamicRoutes: DynamicRoutes | undefined
  } {
S
Steven 已提交
491
    const server: Server = this
492 493 494
    const publicRoutes = fs.existsSync(this.publicDir)
      ? this.generatePublicRoutes()
      : []
J
JJ Kasper 已提交
495

496
    const staticFilesRoute = this.hasStaticDir
497 498 499 500 501
      ? [
          {
            // 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.
502
            // See more: https://github.com/vercel/next.js/issues/2617
503
            match: route('/static/:path*'),
504
            name: 'static catchall',
505
            fn: async (req, res, params, parsedUrl) => {
506
              const p = join(this.dir, 'static', ...params.path)
507
              await this.serveStatic(req, res, p, parsedUrl)
508 509 510
              return {
                finished: true,
              }
511 512 513 514
            },
          } as Route,
        ]
      : []
515

516
    const fsRoutes: Route[] = [
T
Tim Neutkens 已提交
517
      {
518
        match: route('/_next/static/:path*'),
519 520
        type: 'route',
        name: '_next/static catchall',
521
        fn: async (req, res, params, parsedUrl) => {
522
          // make sure to 404 for /_next/static itself
523 524 525 526 527 528
          if (!params.path) {
            await this.render404(req, res, parsedUrl)
            return {
              finished: true,
            }
          }
529

J
Joe Haddad 已提交
530 531 532
          if (
            params.path[0] === CLIENT_STATIC_FILES_RUNTIME ||
            params.path[0] === 'chunks' ||
533 534
            params.path[0] === 'css' ||
            params.path[0] === 'media' ||
535
            params.path[0] === this.buildId ||
536
            params.path[0] === 'pages' ||
537
            params.path[1] === 'pages'
J
Joe Haddad 已提交
538
          ) {
T
Tim Neutkens 已提交
539
            this.setImmutableAssetCacheControl(res)
540
          }
J
Joe Haddad 已提交
541 542 543
          const p = join(
            this.distDir,
            CLIENT_STATIC_FILES_PATH,
544
            ...(params.path || [])
J
Joe Haddad 已提交
545
          )
546
          await this.serveStatic(req, res, p, parsedUrl)
547 548 549
          return {
            finished: true,
          }
550
        },
551
      },
J
JJ Kasper 已提交
552 553
      {
        match: route('/_next/data/:path*'),
554 555
        type: 'route',
        name: '_next/data catchall',
J
JJ Kasper 已提交
556
        fn: async (req, res, params, _parsedUrl) => {
J
JJ Kasper 已提交
557 558 559
          // 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) {
560 561 562 563
            await this.render404(req, res, _parsedUrl)
            return {
              finished: true,
            }
J
JJ Kasper 已提交
564 565 566 567 568 569
          }
          // 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')) {
570 571 572 573
            await this.render404(req, res, _parsedUrl)
            return {
              finished: true,
            }
J
JJ Kasper 已提交
574 575 576
          }

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

J
Joe Haddad 已提交
579
          const { i18n } = this.nextConfig
580 581

          if (i18n) {
582 583 584
            const { host } = req?.headers || {}
            // remove port from host and remove port if present
            const hostname = host?.split(':')[0].toLowerCase()
585
            const localePathResult = normalizeLocalePath(pathname, i18n.locales)
586
            const { defaultLocale } =
587
              detectDomainLocale(i18n.domains, hostname) || {}
588
            let detectedLocale = defaultLocale
589 590 591 592 593

            if (localePathResult.detectedLocale) {
              pathname = localePathResult.pathname
              detectedLocale = localePathResult.detectedLocale
            }
594
            _parsedUrl.query.__nextLocale = detectedLocale!
595 596
          }
          pathname = getRouteFromAssetPath(pathname, '.json')
J
JJ Kasper 已提交
597

J
JJ Kasper 已提交
598
          const parsedUrl = parseUrl(pathname, true)
599

J
JJ Kasper 已提交
600 601 602 603
          await this.render(
            req,
            res,
            pathname,
604
            { ..._parsedUrl.query, _nextDataReq: '1' },
J
JJ Kasper 已提交
605 606
            parsedUrl
          )
607 608 609
          return {
            finished: true,
          }
J
JJ Kasper 已提交
610 611
        },
      },
S
Steven 已提交
612 613 614 615 616 617 618
      {
        match: route('/_next/image'),
        type: 'route',
        name: '_next/image catchall',
        fn: (req, res, _params, parsedUrl) =>
          imageOptimizer(server, req, res, parsedUrl),
      },
T
Tim Neutkens 已提交
619
      {
620
        match: route('/_next/:path*'),
621 622
        type: 'route',
        name: '_next catchall',
T
Tim Neutkens 已提交
623
        // This path is needed because `render()` does a check for `/_next` and the calls the routing again
624
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
625
          await this.render404(req, res, parsedUrl)
626 627 628
          return {
            finished: true,
          }
L
Lukáš Huvar 已提交
629 630
        },
      },
631 632
      ...publicRoutes,
      ...staticFilesRoute,
T
Tim Neutkens 已提交
633
    ]
634

635 636 637 638 639 640
    const getCustomRouteBasePath = (r: { basePath?: false }) => {
      return r.basePath !== false && this.renderOpts.dev
        ? this.nextConfig.basePath
        : ''
    }

641 642 643 644
    const getCustomRoute = (r: Rewrite | Redirect | Header, type: RouteType) =>
      ({
        ...r,
        type,
645
        match: getCustomRouteMatcher(`${getCustomRouteBasePath(r)}${r.source}`),
646 647 648 649 650 651 652
        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
653
      }
654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674

      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}`
            )
        }
675
      }
676 677 678 679 680 681 682 683 684 685 686 687
      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)
688
    }
689

690 691 692 693 694 695 696 697 698 699 700 701 702 703 704
    // 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)
705
            }
706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723
            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,
724 725 726
            parsedUrl.query,
            false,
            getCustomRouteBasePath(redirectRoute)
727
          )
728 729

          const { query } = parsedDestination
730
          delete (parsedDestination as any).query
731

732
          parsedDestination.search = stringifyQs(query)
733

734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755
          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 {
756
        ...rewriteRoute,
757 758 759 760 761 762 763 764 765
        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,
766 767
            true,
            getCustomRouteBasePath(rewriteRoute)
768
          )
769

770 771
          // external rewrite, proxy it
          if (parsedDestination.protocol) {
772
            const { query } = parsedDestination
773
            delete (parsedDestination as any).query
774
            parsedDestination.search = stringifyQs(query)
775

776 777 778 779 780 781 782 783 784 785 786
            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)
            })
787 788 789
            return {
              finished: true,
            }
790 791
          }
          ;(req as any)._nextRewroteUrl = newUrl
792 793
          ;(req as any)._nextDidRewrite =
            (req as any)._nextRewroteUrl !== req.url
794

795 796 797 798 799 800 801 802
          return {
            finished: false,
            pathname: newUrl,
            query: parsedDestination.query,
          }
        },
      } as Route
    })
803 804 805 806 807 808

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

J
Jan Potoms 已提交
814
        // next.js core assumes page path without trailing slash
815
        pathname = removePathTrailingSlash(pathname)
J
Jan Potoms 已提交
816

817
        if (params?.path?.[0] === 'api') {
818 819 820
          const handled = await this.handleApiRequest(
            req as NextApiRequest,
            res as NextApiResponse,
821
            pathname,
822
            query
823 824 825 826 827 828 829
          )
          if (handled) {
            return { finished: true }
          }
        }

        await this.render(req, res, pathname, query, parsedUrl)
830 831 832 833
        return {
          finished: true,
        }
      },
834
    }
835

836
    const { useFileSystemPublicRoutes } = this.nextConfig
J
Joe Haddad 已提交
837

838 839
    if (useFileSystemPublicRoutes) {
      this.dynamicRoutes = this.getDynamicRoutes()
840
    }
N
nkzawa 已提交
841

842
    return {
843
      headers,
844
      fsRoutes,
845 846
      rewrites,
      redirects,
847
      catchAllRoute,
848
      useFileSystemPublicRoutes,
849
      dynamicRoutes: this.dynamicRoutes,
850
      basePath: this.nextConfig.basePath,
851 852
      pageChecker: this.hasPage.bind(this),
    }
T
Tim Neutkens 已提交
853 854
  }

855
  private async getPagePath(pathname: string): Promise<string> {
856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872
    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
  }

873 874 875 876 877
  protected async _beforeCatchAllRender(
    _req: IncomingMessage,
    _res: ServerResponse,
    _params: Params,
    _parsedUrl: UrlWithParsedQuery
878
  ): Promise<boolean> {
879 880 881
    return false
  }

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

L
Lukáš Huvar 已提交
885 886 887 888 889 890
  /**
   * Resolves `API` request, in development builds on demand
   * @param req http request
   * @param res http response
   * @param pathname path of request
   */
J
Joe Haddad 已提交
891
  private async handleApiRequest(
892 893
    req: IncomingMessage,
    res: ServerResponse,
894 895
    pathname: string,
    query: ParsedUrlQuery
896
  ): Promise<boolean> {
897
    let page = pathname
L
Lukáš Huvar 已提交
898
    let params: Params | boolean = false
899
    let pageFound = await this.hasPage(page)
J
JJ Kasper 已提交
900

901
    if (!pageFound && this.dynamicRoutes) {
L
Lukáš Huvar 已提交
902 903
      for (const dynamicRoute of this.dynamicRoutes) {
        params = dynamicRoute.match(pathname)
904
        if (dynamicRoute.page.startsWith('/api') && params) {
905 906
          page = dynamicRoute.page
          pageFound = true
L
Lukáš Huvar 已提交
907 908 909 910 911
          break
        }
      }
    }

912
    if (!pageFound) {
913
      return false
J
JJ Kasper 已提交
914
    }
915 916 917 918
    // Make sure the page is built before getting the path
    // or else it won't be in the manifest yet
    await this.ensureApiPage(page)

919 920 921 922 923 924 925 926 927 928
    let builtPagePath
    try {
      builtPagePath = await this.getPagePath(page)
    } catch (err) {
      if (err.code === 'ENOENT') {
        return false
      }
      throw err
    }

929
    const pageModule = await require(builtPagePath)
930
    query = { ...query, ...params }
J
JJ Kasper 已提交
931

932
    if (!this.renderOpts.dev && this._isLikeServerless) {
933
      if (typeof pageModule.default === 'function') {
934
        prepareServerlessUrl(req, query)
935 936
        await pageModule.default(req, res)
        return true
J
JJ Kasper 已提交
937 938 939
      }
    }

J
Joe Haddad 已提交
940 941 942 943 944
    await apiResolver(
      req,
      res,
      query,
      pageModule,
945
      this.renderOpts.previewProps,
946
      false,
J
Joe Haddad 已提交
947 948
      this.onErrorMiddleware
    )
949
    return true
L
Lukáš Huvar 已提交
950 951
  }

952
  protected generatePublicRoutes(): Route[] {
953
    const publicFiles = new Set(
954 955 956
      recursiveReadDirSync(this.publicDir).map((p) =>
        encodeURI(p.replace(/\\/g, '/'))
      )
957 958 959 960 961 962 963
    )

    return [
      {
        match: route('/:path*'),
        name: 'public folder catchall',
        fn: async (req, res, params, parsedUrl) => {
964
          const pathParts: string[] = params.path || []
965 966 967 968 969 970 971 972
          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()
          }

973
          const path = `/${pathParts.join('/')}`
974 975 976 977 978

          if (publicFiles.has(path)) {
            await this.serveStatic(
              req,
              res,
979
              join(this.publicDir, ...pathParts),
980 981
              parsedUrl
            )
982 983 984
            return {
              finished: true,
            }
985 986 987 988 989 990 991
          }
          return {
            finished: false,
          }
        },
      } as Route,
    ]
992 993
  }

994
  protected getDynamicRoutes() {
995 996
    return getSortedRoutes(Object.keys(this.pagesManifest!))
      .filter(isDynamicRoute)
J
Joe Haddad 已提交
997
      .map((page) => ({
998 999 1000
        page,
        match: getRouteMatcher(getRouteRegex(page)),
      }))
J
Joe Haddad 已提交
1001 1002
  }

1003
  private handleCompression(req: IncomingMessage, res: ServerResponse): void {
1004 1005 1006 1007 1008
    if (this.compression) {
      this.compression(req, res, () => {})
    }
  }

1009
  protected async run(
J
Joe Haddad 已提交
1010 1011
    req: IncomingMessage,
    res: ServerResponse,
1012
    parsedUrl: UrlWithParsedQuery
1013
  ): Promise<void> {
1014 1015
    this.handleCompression(req, res)

1016
    try {
1017 1018
      const matched = await this.router.execute(req, res, parsedUrl)
      if (matched) {
1019 1020 1021 1022 1023 1024 1025 1026
        return
      }
    } catch (err) {
      if (err.code === 'DECODE_FAILED') {
        res.statusCode = 400
        return this.renderError(null, req, res, '/_error', {})
      }
      throw err
1027 1028
    }

1029
    await this.render404(req, res, parsedUrl)
N
nkzawa 已提交
1030 1031
  }

1032
  protected async sendHTML(
J
Joe Haddad 已提交
1033 1034
    req: IncomingMessage,
    res: ServerResponse,
1035
    html: string
1036
  ): Promise<void> {
T
Tim Neutkens 已提交
1037
    const { generateEtags, poweredByHeader } = this.renderOpts
1038 1039 1040 1041
    return sendPayload(req, res, html, 'html', {
      generateEtags,
      poweredByHeader,
    })
1042 1043
  }

J
Joe Haddad 已提交
1044 1045 1046 1047 1048
  public async render(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
1049
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1050
  ): Promise<void> {
1051 1052 1053 1054 1055 1056
    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`
      )
    }

1057 1058 1059 1060 1061 1062 1063 1064 1065 1066
    if (
      this.renderOpts.customServer &&
      pathname === '/index' &&
      !(await this.hasPage('/index'))
    ) {
      // maintain backwards compatibility for custom server
      // (see custom-server integration tests)
      pathname = '/'
    }

1067
    const url: any = req.url
1068

1069 1070 1071 1072
    // 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
1073
    if (
1074 1075 1076
      !query._nextDataReq &&
      (url.match(/^\/_next\//) ||
        (this.hasStaticDir && url.match(/^\/static\//)))
1077
    ) {
1078 1079 1080
      return this.handleRequest(req, res, parsedUrl)
    }

1081
    if (isBlockedPage(pathname)) {
1082
      return this.render404(req, res, parsedUrl)
1083 1084
    }

1085
    const html = await this.renderToHTML(req, res, pathname, query)
1086 1087
    // Request was ended by the user
    if (html === null) {
1088 1089 1090
      return
    }

1091
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
1092
  }
N
nkzawa 已提交
1093

J
Joe Haddad 已提交
1094
  private async findPageComponents(
J
Joe Haddad 已提交
1095
    pathname: string,
1096 1097 1098
    query: ParsedUrlQuery = {},
    params: Params | null = null
  ): Promise<FindComponentsResult | null> {
1099
    let paths = [
1100 1101 1102 1103
      // try serving a static AMP version first
      query.amp ? normalizePagePath(pathname) + '.amp' : null,
      pathname,
    ].filter(Boolean)
1104 1105 1106 1107 1108 1109 1110 1111 1112 1113

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

1114
    for (const pagePath of paths) {
J
JJ Kasper 已提交
1115
      try {
1116
        const components = await loadComponents(
J
Joe Haddad 已提交
1117
          this.distDir,
1118 1119
          pagePath!,
          !this.renderOpts.dev && this._isLikeServerless
J
Joe Haddad 已提交
1120
        )
1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132
        // 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
        }

1133 1134 1135
        return {
          components,
          query: {
1136
            ...(components.getStaticProps
1137 1138 1139 1140 1141
              ? {
                  amp: query.amp,
                  _nextDataReq: query._nextDataReq,
                  __nextLocale: query.__nextLocale,
                }
1142 1143 1144 1145
              : query),
            ...(params || {}),
          },
        }
J
JJ Kasper 已提交
1146 1147 1148 1149
      } catch (err) {
        if (err.code !== 'ENOENT') throw err
      }
    }
1150
    return null
J
Joe Haddad 已提交
1151 1152
  }

1153
  protected async getStaticPaths(
1154 1155 1156
    pathname: string
  ): Promise<{
    staticPaths: string[] | undefined
1157
    fallbackMode: 'static' | 'blocking' | false
1158
  }> {
1159 1160 1161 1162 1163
    // `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.
1164 1165
    const fallbackField = this.getPrerenderManifest().dynamicRoutes[pathname]
      .fallback
1166

1167 1168 1169 1170 1171 1172 1173 1174 1175
    return {
      staticPaths,
      fallbackMode:
        typeof fallbackField === 'string'
          ? 'static'
          : fallbackField === null
          ? 'blocking'
          : false,
    }
1176 1177
  }

J
Joe Haddad 已提交
1178 1179 1180 1181
  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1182
    { components, query }: FindComponentsResult,
1183
    opts: RenderOptsPartial
1184
  ): Promise<string | null> {
1185 1186
    const is404Page = pathname === '/404'

1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197
    const isLikeServerless =
      typeof components.Component === 'object' &&
      typeof (components.Component as any).renderReqToHTML === 'function'
    const isSSG = !!components.getStaticProps
    const isServerProps = !!components.getServerSideProps
    const hasStaticPaths = !!components.getStaticPaths

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

1198
    // we need to ensure the status code if /404 is visited directly
1199
    if (is404Page && !isDataReq) {
1200 1201 1202
      res.statusCode = 404
    }

J
JJ Kasper 已提交
1203
    // handle static page
1204 1205
    if (typeof components.Component === 'string') {
      return components.Component
J
Joe Haddad 已提交
1206 1207
    }

1208 1209 1210 1211
    if (!query.amp) {
      delete query.amp
    }

J
JJ Kasper 已提交
1212 1213
    const locale = query.__nextLocale as string
    delete query.__nextLocale
1214

J
Joe Haddad 已提交
1215
    const { i18n } = this.nextConfig
1216
    const locales = i18n.locales as string[]
J
JJ Kasper 已提交
1217

1218 1219 1220 1221 1222 1223 1224 1225
    let previewData: string | false | object | undefined
    let isPreviewMode = false

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

1226 1227 1228
    // 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
1229 1230 1231
    let urlPathname = parseUrl(req.url || '').pathname || '/'

    let resolvedUrlPathname = (req as any)._nextRewroteUrl
1232
      ? (req as any)._nextRewroteUrl
1233
      : urlPathname
1234

1235 1236 1237 1238 1239 1240 1241 1242 1243
    resolvedUrlPathname = removePathTrailingSlash(resolvedUrlPathname)
    urlPathname = removePathTrailingSlash(urlPathname)

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

J
Joe Haddad 已提交
1245
      if (this.nextConfig.i18n) {
J
JJ Kasper 已提交
1246
        return normalizeLocalePath(path, locales).pathname
1247
      }
1248 1249
      return path
    }
1250

1251 1252
    // remove /_next/data prefix from urlPathname so it matches
    // for direct page visit and /_next/data visit
1253 1254 1255
    if (isDataReq) {
      resolvedUrlPathname = stripNextDataPath(resolvedUrlPathname)
      urlPathname = stripNextDataPath(urlPathname)
1256 1257
    }

1258
    let ssgCacheKey =
1259 1260
      isPreviewMode || !isSSG
        ? undefined // Preview mode bypasses the cache
1261 1262 1263
        : `${locale ? `/${locale}` : ''}${resolvedUrlPathname}${
            query.amp ? '.amp' : ''
          }`
J
JJ Kasper 已提交
1264

1265 1266 1267 1268 1269 1270
    if (is404Page && isSSG) {
      ssgCacheKey = `${locale ? `/${locale}` : ''}${pathname}${
        query.amp ? '.amp' : ''
      }`
    }

J
JJ Kasper 已提交
1271
    // Complete the response with cached data if its present
1272 1273 1274
    const cachedData = ssgCacheKey
      ? await this.incrementalCache.get(ssgCacheKey)
      : undefined
1275

J
JJ Kasper 已提交
1276
    if (cachedData) {
1277 1278 1279 1280 1281 1282
      if (cachedData.isNotFound) {
        // we don't currently revalidate when notFound is returned
        // so trigger rendering 404 here
        throw new NoFallbackError()
      }

1283
      const data = isDataReq
J
JJ Kasper 已提交
1284 1285 1286
        ? JSON.stringify(cachedData.pageData)
        : cachedData.html

1287
      sendPayload(
1288
        req,
J
JJ Kasper 已提交
1289 1290
        res,
        data,
1291
        isDataReq ? 'json' : 'html',
1292 1293 1294 1295
        {
          generateEtags: this.renderOpts.generateEtags,
          poweredByHeader: this.renderOpts.poweredByHeader,
        },
1296 1297 1298 1299 1300 1301 1302 1303 1304
        !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,
            }
1305
          : undefined
J
JJ Kasper 已提交
1306 1307 1308 1309 1310 1311
      )

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

J
JJ Kasper 已提交
1314
    // If we're here, that means data is missing or it's stale.
1315
    const maybeCoalesceInvoke = ssgCacheKey
1316
      ? (fn: any) => withCoalescedInvoke(fn).bind(null, ssgCacheKey!, [])
1317 1318 1319 1320
      : (fn: any) => async () => {
          const value = await fn()
          return { isOrigin: true, value }
        }
J
JJ Kasper 已提交
1321

1322 1323 1324 1325 1326
    const doRender = maybeCoalesceInvoke(
      async (): Promise<{
        html: string | null
        pageData: any
        sprRevalidate: number | false
1327
        isNotFound?: boolean
1328 1329 1330 1331
      }> => {
        let pageData: any
        let html: string | null
        let sprRevalidate: number | false
1332
        let isNotFound: boolean | undefined
1333 1334 1335 1336 1337 1338 1339

        let renderResult
        // handle serverless
        if (isLikeServerless) {
          renderResult = await (components.Component as any).renderReqToHTML(
            req,
            res,
P
Prateek Bhatnagar 已提交
1340 1341 1342
            'passthrough',
            {
              fontManifest: this.renderOpts.fontManifest,
1343
              locale,
1344 1345
              locales,
              // defaultLocale,
P
Prateek Bhatnagar 已提交
1346
            }
1347
          )
J
JJ Kasper 已提交
1348

1349 1350 1351
          html = renderResult.html
          pageData = renderResult.renderOpts.pageData
          sprRevalidate = renderResult.renderOpts.revalidate
1352
          isNotFound = renderResult.renderOpts.isNotFound
1353
        } else {
1354 1355 1356 1357 1358 1359 1360
          const origQuery = parseUrl(req.url || '', true).query
          const resolvedUrl = formatUrl({
            pathname: resolvedUrlPathname,
            // make sure to only add query values from original URL
            query: origQuery,
          })

1361 1362 1363 1364
          const renderOpts: RenderOpts = {
            ...components,
            ...opts,
            isDataReq,
1365
            resolvedUrl,
1366
            locale,
1367 1368
            locales,
            // defaultLocale,
1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379
            // 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,
1380
          }
1381

1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393
          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
1394
          isNotFound = (renderOpts as any).isNotFound
J
JJ Kasper 已提交
1395 1396
        }

1397
        return { html, pageData, sprRevalidate, isNotFound }
J
JJ Kasper 已提交
1398
      }
1399
    )
J
JJ Kasper 已提交
1400

1401
    const isProduction = !this.renderOpts.dev
J
Joe Haddad 已提交
1402
    const isDynamicPathname = isDynamicRoute(pathname)
1403
    const didRespond = isResSent(res)
1404

1405
    const { staticPaths, fallbackMode } = hasStaticPaths
1406
      ? await this.getStaticPaths(pathname)
1407
      : { staticPaths: undefined, fallbackMode: false }
1408

1409 1410 1411 1412 1413
    // When we did not respond from cache, we need to choose to block on
    // rendering or return a skeleton.
    //
    // * Data requests always block.
    //
1414 1415
    // * Blocking mode fallback always blocks.
    //
1416 1417
    // * Preview mode toggles all pages to be resolved in a blocking manner.
    //
1418
    // * Non-dynamic pages should block (though this is an impossible
1419 1420
    //   case in production).
    //
1421 1422
    // * Dynamic pages should return their skeleton if not defined in
    //   getStaticPaths, then finish the data request on the client-side.
1423
    //
J
Joe Haddad 已提交
1424
    if (
1425
      fallbackMode !== 'blocking' &&
1426
      ssgCacheKey &&
1427 1428 1429
      !didRespond &&
      !isPreviewMode &&
      isDynamicPathname &&
1430 1431
      // Development should trigger fallback when the path is not in
      // `getStaticPaths`
1432 1433
      (isProduction ||
        !staticPaths ||
1434 1435 1436 1437 1438
        // static paths always includes locale so make sure it's prefixed
        // with it
        !staticPaths.includes(
          `${locale ? '/' + locale : ''}${resolvedUrlPathname}`
        ))
J
Joe Haddad 已提交
1439
    ) {
1440 1441 1442 1443 1444
      if (
        // In development, fall through to render to handle missing
        // getStaticPaths.
        (isProduction || staticPaths) &&
        // When fallback isn't present, abort this render so we 404
1445
        fallbackMode !== 'static'
1446
      ) {
1447
        throw new NoFallbackError()
1448 1449
      }

1450 1451
      if (!isDataReq) {
        let html: string
1452

1453 1454
        // Production already emitted the fallback as static HTML.
        if (isProduction) {
1455 1456 1457
          html = await this.incrementalCache.getFallback(
            locale ? `/${locale}${pathname}` : pathname
          )
1458 1459 1460 1461 1462 1463 1464 1465 1466
        }
        // 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
1467 1468
        }

1469 1470 1471 1472 1473 1474
        sendPayload(req, res, html, 'html', {
          generateEtags: this.renderOpts.generateEtags,
          poweredByHeader: this.renderOpts.poweredByHeader,
        })
        return null
      }
1475 1476
    }

1477 1478
    const {
      isOrigin,
1479
      value: { html, pageData, sprRevalidate, isNotFound },
1480
    } = await doRender()
1481
    let resHtml = html
1482 1483 1484 1485 1486 1487

    if (
      !isResSent(res) &&
      !isNotFound &&
      (isSSG || isDataReq || isServerProps)
    ) {
1488
      sendPayload(
1489
        req,
1490 1491
        res,
        isDataReq ? JSON.stringify(pageData) : html,
1492
        isDataReq ? 'json' : 'html',
1493 1494 1495 1496
        {
          generateEtags: this.renderOpts.generateEtags,
          poweredByHeader: this.renderOpts.poweredByHeader,
        },
1497
        !this.renderOpts.dev || (isServerProps && !isDataReq)
1498 1499
          ? {
              private: isPreviewMode,
1500
              stateful: !isSSG,
1501 1502
              revalidate: sprRevalidate,
            }
1503
          : undefined
1504
      )
1505
      resHtml = null
1506
    }
J
JJ Kasper 已提交
1507

1508
    // Update the cache if the head request and cacheable
1509
    if (isOrigin && ssgCacheKey) {
1510 1511
      await this.incrementalCache.set(
        ssgCacheKey,
1512
        { html: html!, pageData, isNotFound },
1513 1514
        sprRevalidate
      )
1515 1516
    }

1517 1518 1519
    if (isNotFound) {
      throw new NoFallbackError()
    }
1520
    return resHtml
1521 1522
  }

1523
  public async renderToHTML(
J
Joe Haddad 已提交
1524 1525 1526
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1527
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1528
  ): Promise<string | null> {
1529 1530 1531
    try {
      const result = await this.findPageComponents(pathname, query)
      if (result) {
1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543
        try {
          return await this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
            result,
            { ...this.renderOpts }
          )
        } catch (err) {
          if (!(err instanceof NoFallbackError)) {
            throw err
          }
1544
        }
1545
      }
J
Joe Haddad 已提交
1546

1547 1548 1549 1550 1551 1552
      if (this.dynamicRoutes) {
        for (const dynamicRoute of this.dynamicRoutes) {
          const params = dynamicRoute.match(pathname)
          if (!params) {
            continue
          }
J
Joe Haddad 已提交
1553

1554
          const dynamicRouteResult = await this.findPageComponents(
1555 1556 1557 1558
            dynamicRoute.page,
            query,
            params
          )
1559
          if (dynamicRouteResult) {
1560 1561 1562 1563 1564
            try {
              return await this.renderToHTMLWithComponents(
                req,
                res,
                dynamicRoute.page,
1565
                dynamicRouteResult,
1566 1567 1568 1569 1570 1571
                { ...this.renderOpts, params }
              )
            } catch (err) {
              if (!(err instanceof NoFallbackError)) {
                throw err
              }
1572
            }
J
Joe Haddad 已提交
1573 1574
          }
        }
1575 1576 1577
      }
    } catch (err) {
      this.logError(err)
1578 1579 1580 1581 1582

      if (err && err.code === 'DECODE_FAILED') {
        res.statusCode = 400
        return await this.renderErrorToHTML(err, req, res, pathname, query)
      }
1583 1584 1585 1586 1587
      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 已提交
1588 1589
  }

J
Joe Haddad 已提交
1590 1591 1592 1593 1594
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1595
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1596 1597 1598
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
1599
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
1600
    )
N
Naoyuki Kanezawa 已提交
1601
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
1602
    if (html === null) {
1603 1604
      return
    }
1605
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
1606 1607
  }

1608 1609 1610 1611 1612 1613 1614 1615 1616
  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 已提交
1617 1618 1619 1620 1621
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
1622
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1623
  ) {
1624
    let result: null | FindComponentsResult = null
1625

1626 1627 1628
    const is404 = res.statusCode === 404
    let using404Page = false

1629
    // use static 404 page if available and is 404 response
1630
    if (is404) {
1631
      result = await this.findPageComponents('/404', query)
1632
      using404Page = result !== null
1633 1634 1635 1636 1637 1638
    }

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

1639 1640 1641
    if (
      process.env.NODE_ENV !== 'production' &&
      !using404Page &&
1642 1643
      (await this.hasPage('/_error')) &&
      !(await this.hasPage('/404'))
1644 1645 1646 1647
    ) {
      this.customErrorNo404Warn()
    }

1648
    let html: string | null
1649
    try {
1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660
      try {
        html = await this.renderToHTMLWithComponents(
          req,
          res,
          using404Page ? '/404' : '/_error',
          result!,
          {
            ...this.renderOpts,
            err,
          }
        )
1661 1662
      } catch (maybeFallbackError) {
        if (maybeFallbackError instanceof NoFallbackError) {
1663
          throw new Error('invariant: failed to render error page')
1664
        }
1665
        throw maybeFallbackError
1666
      }
1667 1668
    } catch (renderToHtmlError) {
      console.error(renderToHtmlError)
1669 1670 1671 1672
      res.statusCode = 500
      html = 'Internal Server Error'
    }
    return html
N
Naoyuki Kanezawa 已提交
1673 1674
  }

J
Joe Haddad 已提交
1675 1676 1677
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
1678
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1679
  ): Promise<void> {
1680 1681
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
N
Naoyuki Kanezawa 已提交
1682
    res.statusCode = 404
1683
    return this.renderError(null, req, res, pathname!, query)
N
Naoyuki Kanezawa 已提交
1684
  }
N
Naoyuki Kanezawa 已提交
1685

J
Joe Haddad 已提交
1686 1687 1688 1689
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
1690
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1691
  ): Promise<void> {
A
Arunoda Susiripala 已提交
1692
    if (!this.isServeableUrl(path)) {
1693
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
1694 1695
    }

1696 1697 1698 1699 1700 1701
    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 已提交
1702
    try {
1703
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
1704
    } catch (err) {
T
Tim Neutkens 已提交
1705
      if (err.code === 'ENOENT' || err.statusCode === 404) {
1706
        this.render404(req, res, parsedUrl)
1707 1708 1709
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
1710 1711 1712 1713 1714 1715
      } else {
        throw err
      }
    }
  }

1716 1717 1718 1719 1720 1721 1722 1723 1724
  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 已提交
1725
      userFilesStatic = recursiveReadDirSync(pathUserFilesStatic).map((f) =>
1726 1727 1728 1729 1730 1731
        join('.', 'static', f)
      )
    }

    let userFilesPublic: string[] = []
    if (this.publicDir && fs.existsSync(this.publicDir)) {
J
Joe Haddad 已提交
1732
      userFilesPublic = recursiveReadDirSync(this.publicDir).map((f) =>
1733 1734 1735 1736 1737 1738 1739
        join('.', 'public', f)
      )
    }

    let nextFilesStatic: string[] = []
    nextFilesStatic = recursiveReadDirSync(
      join(this.distDir, 'static')
J
Joe Haddad 已提交
1740
    ).map((f) => join('.', relative(this.dir, this.distDir), 'static', f))
1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774

    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 已提交
1775
    if (
1776 1777 1778
      (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 已提交
1779 1780 1781 1782
    ) {
      return false
    }

1783 1784 1785 1786
    // Check against the real filesystem paths
    const filesystemUrls = this.getFilesystemPaths()
    const resolved = relative(this.dir, untrustedFilePath)
    return filesystemUrls.has(resolved)
A
Arunoda Susiripala 已提交
1787 1788
  }

1789
  protected readBuildId(): string {
1790 1791 1792 1793 1794
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
1795
        throw new Error(
1796
          `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 已提交
1797
        )
1798 1799 1800
      }

      throw err
1801
    }
1802
  }
1803

1804
  protected get _isLikeServerless(): boolean {
1805 1806
    return isTargetLikeServerless(this.nextConfig.target)
  }
1807
}
1808

1809 1810 1811 1812
function prepareServerlessUrl(
  req: IncomingMessage,
  query: ParsedUrlQuery
): void {
1813 1814 1815 1816 1817 1818 1819 1820 1821 1822
  const curUrl = parseUrl(req.url!, true)
  req.url = formatUrl({
    ...curUrl,
    search: undefined,
    query: {
      ...curUrl.query,
      ...query,
    },
  })
}
1823 1824

class NoFallbackError extends Error {}