next-server.ts 56.0 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 '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,
29
  PERMANENT_REDIRECT_STATUS,
J
Joe Haddad 已提交
30
  PHASE_PRODUCTION_SERVER,
J
Joe Haddad 已提交
31
  PRERENDER_MANIFEST,
32
  ROUTES_MANIFEST,
33
  SERVERLESS_DIRECTORY,
J
Joe Haddad 已提交
34
  SERVER_DIRECTORY,
T
Tim Neutkens 已提交
35
} from '../lib/constants'
J
Joe Haddad 已提交
36 37 38 39
import {
  getRouteMatcher,
  getRouteRegex,
  getSortedRoutes,
40
  isDynamicRoute,
J
Joe Haddad 已提交
41
} from '../lib/router/utils'
42
import * as envConfig from '../lib/runtime-config'
J
Joe Haddad 已提交
43
import { isResSent, NextApiRequest, NextApiResponse } from '../lib/utils'
44 45 46 47 48 49 50
import {
  apiResolver,
  setLazyProp,
  getCookieParser,
  tryGetPreviewData,
  __ApiPreviewProps,
} from './api-utils'
51
import loadConfig, { isTargetLikeServerless } from './config'
52
import pathMatch from '../lib/router/utils/path-match'
J
Joe Haddad 已提交
53
import { recursiveReadDirSync } from './lib/recursive-readdir-sync'
54
import { loadComponents, LoadComponentsReturnType } from './load-components'
J
Joe Haddad 已提交
55
import { normalizePagePath } from './normalize-page-path'
56
import { RenderOpts, RenderOptsPartial, renderToHTML } from './render'
P
Prateek Bhatnagar 已提交
57
import { getPagePath, requireFontManifest } from './require'
58 59 60
import Router, {
  DynamicRoutes,
  PageChecker,
J
Joe Haddad 已提交
61 62 63
  Params,
  route,
  Route,
64
} from './router'
65 66 67
import prepareDestination, {
  compileNonPath,
} from '../lib/router/utils/prepare-destination'
68
import { sendPayload } from './send-payload'
J
Joe Haddad 已提交
69
import { serveStatic } from './serve-static'
70
import { IncrementalCache } from './incremental-cache'
71
import { execOnce } from '../lib/utils'
72
import { isBlockedPage } from './utils'
73
import { loadEnvConfig } from '@next/env'
74
import './node-polyfill-fetch'
J
Jan Potoms 已提交
75
import { PagesManifest } from '../../build/webpack/plugins/pages-manifest-plugin'
76
import { removePathTrailingSlash } from '../../client/normalize-trailing-slash'
77
import getRouteFromAssetPath from '../lib/router/utils/get-route-from-asset-path'
P
Prateek Bhatnagar 已提交
78
import { FontManifest } from './font-utils'
79
import { denormalizePagePath } from './denormalize-page-path'
80 81 82
import accept from '@hapi/accept'
import { normalizeLocalePath } from '../lib/i18n/normalize-locale-path'
import { detectLocaleCookie } from '../lib/i18n/detect-locale-cookie'
83
import * as Log from '../../build/output/log'
S
Steven 已提交
84
import { imageOptimizer } from './image-optimizer'
85
import { detectDomainLocale } from '../lib/i18n/detect-domain-locale'
86
import cookie from 'next/dist/compiled/cookie'
87
import escapeStringRegexp from 'next/dist/compiled/escape-string-regexp'
J
JJ Kasper 已提交
88 89

const getCustomRouteMatcher = pathMatch(true)
90 91 92

type NextConfig = any

93 94 95 96 97 98
type Middleware = (
  req: IncomingMessage,
  res: ServerResponse,
  next: (err?: Error) => void
) => void

99 100 101 102 103
type FindComponentsResult = {
  components: LoadComponentsReturnType
  query: ParsedUrlQuery
}

104 105 106 107 108
type DynamicRouteItem = {
  page: string
  match: ReturnType<typeof getRouteMatcher>
}

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

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

J
Joe Haddad 已提交
164 165 166 167
  public constructor({
    dir = '.',
    quiet = false,
    conf = null,
J
JJ Kasper 已提交
168
    dev = false,
169
    customServer = true,
J
Joe Haddad 已提交
170
  }: ServerConstructor = {}) {
N
nkzawa 已提交
171
    this.dir = resolve(dir)
N
Naoyuki Kanezawa 已提交
172
    this.quiet = quiet
T
Tim Neutkens 已提交
173
    const phase = this.currentPhase()
174
    loadEnvConfig(this.dir, dev, Log)
175

176
    this.nextConfig = loadConfig(phase, this.dir, conf)
177
    this.distDir = join(this.dir, this.nextConfig.distDir)
178
    this.publicDir = join(this.dir, CLIENT_PUBLIC_FILES_PATH)
179
    this.hasStaticDir = fs.existsSync(join(this.dir, 'static'))
T
Tim Neutkens 已提交
180

181 182
    // Only serverRuntimeConfig needs the default
    // publicRuntimeConfig gets it's default in client/index.js
J
Joe Haddad 已提交
183 184 185 186 187
    const {
      serverRuntimeConfig = {},
      publicRuntimeConfig,
      assetPrefix,
      generateEtags,
188
      compress,
J
Joe Haddad 已提交
189
    } = this.nextConfig
190

T
Tim Neutkens 已提交
191
    this.buildId = this.readBuildId()
192

193
    this.renderOpts = {
T
Tim Neutkens 已提交
194
      poweredByHeader: this.nextConfig.poweredByHeader,
195
      canonicalBase: this.nextConfig.amp.canonicalBase,
196
      buildId: this.buildId,
197
      generateEtags,
198
      previewProps: this.getPreviewProps(),
199
      customServer: customServer === true ? true : undefined,
200
      ampOptimizerConfig: this.nextConfig.experimental.amp?.optimizer,
201
      basePath: this.nextConfig.basePath,
A
Alex Castle 已提交
202
      images: JSON.stringify(this.nextConfig.images),
203 204 205 206 207
      optimizeFonts: this.nextConfig.experimental.optimizeFonts && !dev,
      fontManifest:
        this.nextConfig.experimental.optimizeFonts && !dev
          ? requireFontManifest(this.distDir, this._isLikeServerless)
          : null,
208
      optimizeImages: this.nextConfig.experimental.optimizeImages,
209
    }
N
Naoyuki Kanezawa 已提交
210

211 212
    // Only the `publicRuntimeConfig` key is exposed to the client side
    // It'll be rendered as part of __NEXT_DATA__ on the client side
213
    if (Object.keys(publicRuntimeConfig).length > 0) {
214
      this.renderOpts.runtimeConfig = publicRuntimeConfig
215 216
    }

217
    if (compress && this.nextConfig.target === 'server') {
218 219 220
      this.compression = compression() as Middleware
    }

221
    // Initialize next/config with the environment configuration
222 223 224 225
    envConfig.setConfig({
      serverRuntimeConfig,
      publicRuntimeConfig,
    })
226

227 228 229 230 231 232 233 234 235 236
    this.serverBuildDir = join(
      this.distDir,
      this._isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY
    )
    const pagesManifestPath = join(this.serverBuildDir, PAGES_MANIFEST)

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

237
    this.customRoutes = this.getCustomRoutes()
J
JJ Kasper 已提交
238
    this.router = new Router(this.generateRoutes())
239
    this.setAssetPrefix(assetPrefix)
J
JJ Kasper 已提交
240

241 242 243
    // call init-server middleware, this is also handled
    // individually in serverless bundles when deployed
    if (!dev && this.nextConfig.experimental.plugins) {
244 245
      const initServer = require(join(this.serverBuildDir, 'init-server.js'))
        .default
246
      this.onErrorMiddleware = require(join(
247
        this.serverBuildDir,
248 249 250 251 252
        'on-error-server.js'
      )).default
      initServer()
    }

253
    this.incrementalCache = new IncrementalCache({
J
JJ Kasper 已提交
254 255 256 257
      dev,
      distDir: this.distDir,
      pagesDir: join(
        this.distDir,
258
        this._isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY,
J
JJ Kasper 已提交
259 260 261
        'pages'
      ),
      flushToDisk: this.nextConfig.experimental.sprFlushToDisk,
262
      locales: this.nextConfig.i18n?.locales,
J
JJ Kasper 已提交
263
    })
P
Prateek Bhatnagar 已提交
264 265 266 267 268 269 270 271 272 273

    /**
     * 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)
    }
274 275 276
    if (this.renderOpts.optimizeImages) {
      process.env.__NEXT_OPTIMIZE_IMAGES = JSON.stringify(true)
    }
N
Naoyuki Kanezawa 已提交
277
  }
N
nkzawa 已提交
278

279
  protected currentPhase(): string {
280
    return PHASE_PRODUCTION_SERVER
281 282
  }

S
Steven 已提交
283
  public logError(err: Error): void {
284 285 286
    if (this.onErrorMiddleware) {
      this.onErrorMiddleware({ err })
    }
287
    if (this.quiet) return
288
    console.error(err)
289 290
  }

291
  private async handleRequest(
J
Joe Haddad 已提交
292 293
    req: IncomingMessage,
    res: ServerResponse,
294
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
295
  ): Promise<void> {
296 297
    setLazyProp({ req: req as any }, 'cookies', getCookieParser(req))

298
    // Parse url if parsedUrl not provided
299
    if (!parsedUrl || typeof parsedUrl !== 'object') {
300 301
      const url: any = req.url
      parsedUrl = parseUrl(url, true)
302
    }
303

304 305 306
    // 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 已提交
307
    }
308
    ;(req as any).__NEXT_INIT_QUERY = Object.assign({}, parsedUrl.query)
309

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

312 313 314 315 316
    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 已提交
317 318
    }

319
    if (i18n && !req.url?.startsWith('/_next')) {
320
      // get pathname from URL with basePath stripped for locale detection
321 322 323
      let { pathname, ...parsed } = parseUrl(req.url || '/')
      pathname = pathname || '/'

324
      let defaultLocale = i18n.defaultLocale
325
      let detectedLocale = detectLocaleCookie(req, i18n.locales)
326 327 328 329
      let acceptPreferredLocale =
        i18n.localeDetection !== false
          ? accept.language(req.headers['accept-language'], i18n.locales)
          : detectedLocale
330

331 332 333 334 335
      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)
336 337 338 339
      if (detectedDomain) {
        defaultLocale = detectedDomain.defaultLocale
        detectedLocale = defaultLocale
      }
340

341 342
      // if not domain specific locale use accept-language preferred
      detectedLocale = detectedLocale || acceptPreferredLocale
343

344 345 346 347 348 349 350 351 352
      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 已提交
353
        ;(req as any).__nextStrippedLocale = true
354
        parsedUrl.pathname = `${basePath || ''}${localePathResult.pathname}`
355 356 357 358 359
      }

      // 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
360
      if (detectedDomain && pathname === '/') {
361 362 363 364
        const localeToCheck = acceptPreferredLocale
        // const localeToCheck = localePathResult.detectedLocale
        //   ? detectedLocale
        //   : acceptPreferredLocale
365

366 367 368
        const matchedDomain = detectDomainLocale(
          i18n.domains,
          undefined,
369
          localeToCheck
370 371
        )

372 373 374 375 376
        if (
          matchedDomain &&
          (matchedDomain.domain !== detectedDomain.domain ||
            localeToCheck !== matchedDomain.defaultLocale)
        ) {
377 378
          localeDomainRedirect = `http${matchedDomain.http ? '' : 's'}://${
            matchedDomain.domain
379 380
          }/${
            localeToCheck === matchedDomain.defaultLocale ? '' : localeToCheck
381
          }`
382 383 384
        }
      }

385
      const denormalizedPagePath = denormalizePagePath(pathname || '/')
386
      const detectedDefaultLocale =
387 388
        !detectedLocale ||
        detectedLocale.toLowerCase() === defaultLocale.toLowerCase()
389 390 391 392
      const shouldStripDefaultLocale = false
      // detectedDefaultLocale &&
      // denormalizedPagePath.toLowerCase() ===
      //   `/${i18n.defaultLocale.toLowerCase()}`
393

394 395
      const shouldAddLocalePrefix =
        !detectedDefaultLocale && denormalizedPagePath === '/'
396

397
      detectedLocale = detectedLocale || i18n.defaultLocale
398

399 400
      if (
        i18n.localeDetection !== false &&
401 402 403
        (localeDomainRedirect ||
          shouldAddLocalePrefix ||
          shouldStripDefaultLocale)
404
      ) {
405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426
        // 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: '/',
            }),
          ])
        }

427 428 429 430 431
        res.setHeader(
          'Location',
          formatUrl({
            // make sure to include any query values when redirecting
            ...parsed,
432 433 434
            pathname: localeDomainRedirect
              ? localeDomainRedirect
              : shouldStripDefaultLocale
435 436
              ? basePath || `/`
              : `${basePath || ''}/${detectedLocale}`,
437 438 439 440
          })
        )
        res.statusCode = 307
        res.end()
441
        return
442
      }
443

444 445 446
      parsedUrl.query.__nextDefaultLocale =
        detectedDomain?.defaultLocale || i18n.defaultLocale

447 448 449 450
      parsedUrl.query.__nextLocale =
        localePathResult.detectedLocale ||
        detectedDomain?.defaultLocale ||
        defaultLocale
451 452
    }

453
    res.statusCode = 200
454 455 456
    try {
      return await this.run(req, res, parsedUrl)
    } catch (err) {
J
Joe Haddad 已提交
457 458 459
      this.logError(err)
      res.statusCode = 500
      res.end('Internal Server Error')
460
    }
461 462
  }

463
  public getRequestHandler() {
464
    return this.handleRequest.bind(this)
N
nkzawa 已提交
465 466
  }

467
  public setAssetPrefix(prefix?: string): void {
468
    this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
469 470
  }

471
  // Backwards compatibility
472
  public async prepare(): Promise<void> {}
N
nkzawa 已提交
473

T
Tim Neutkens 已提交
474
  // Backwards compatibility
475
  protected async close(): Promise<void> {}
T
Tim Neutkens 已提交
476

477
  protected setImmutableAssetCacheControl(res: ServerResponse): void {
T
Tim Neutkens 已提交
478
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
N
nkzawa 已提交
479 480
  }

481
  protected getCustomRoutes(): CustomRoutes {
J
JJ Kasper 已提交
482 483 484
    return require(join(this.distDir, ROUTES_MANIFEST))
  }

485 486 487 488
  private _cachedPreviewManifest: PrerenderManifest | undefined
  protected getPrerenderManifest(): PrerenderManifest {
    if (this._cachedPreviewManifest) {
      return this._cachedPreviewManifest
J
Joe Haddad 已提交
489
    }
490 491 492 493 494 495
    const manifest = require(join(this.distDir, PRERENDER_MANIFEST))
    return (this._cachedPreviewManifest = manifest)
  }

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

498
  protected generateRoutes(): {
499
    basePath: string
500 501
    headers: Route[]
    rewrites: Route[]
502
    fsRoutes: Route[]
503
    redirects: Route[]
504 505
    catchAllRoute: Route
    pageChecker: PageChecker
506
    useFileSystemPublicRoutes: boolean
507
    dynamicRoutes: DynamicRoutes | undefined
508
    locales: string[]
509
  } {
S
Steven 已提交
510
    const server: Server = this
511 512 513
    const publicRoutes = fs.existsSync(this.publicDir)
      ? this.generatePublicRoutes()
      : []
J
JJ Kasper 已提交
514

515
    const staticFilesRoute = this.hasStaticDir
516 517 518 519 520
      ? [
          {
            // 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.
521
            // See more: https://github.com/vercel/next.js/issues/2617
522
            match: route('/static/:path*'),
523
            name: 'static catchall',
524
            fn: async (req, res, params, parsedUrl) => {
525
              const p = join(this.dir, 'static', ...params.path)
526
              await this.serveStatic(req, res, p, parsedUrl)
527 528 529
              return {
                finished: true,
              }
530 531 532 533
            },
          } as Route,
        ]
      : []
534

535
    const fsRoutes: Route[] = [
T
Tim Neutkens 已提交
536
      {
537
        match: route('/_next/static/:path*'),
538 539
        type: 'route',
        name: '_next/static catchall',
540
        fn: async (req, res, params, parsedUrl) => {
541
          // make sure to 404 for /_next/static itself
542 543 544 545 546 547
          if (!params.path) {
            await this.render404(req, res, parsedUrl)
            return {
              finished: true,
            }
          }
548

J
Joe Haddad 已提交
549 550 551
          if (
            params.path[0] === CLIENT_STATIC_FILES_RUNTIME ||
            params.path[0] === 'chunks' ||
552 553
            params.path[0] === 'css' ||
            params.path[0] === 'media' ||
554
            params.path[0] === this.buildId ||
555
            params.path[0] === 'pages' ||
556
            params.path[1] === 'pages'
J
Joe Haddad 已提交
557
          ) {
T
Tim Neutkens 已提交
558
            this.setImmutableAssetCacheControl(res)
559
          }
J
Joe Haddad 已提交
560 561 562
          const p = join(
            this.distDir,
            CLIENT_STATIC_FILES_PATH,
563
            ...(params.path || [])
J
Joe Haddad 已提交
564
          )
565
          await this.serveStatic(req, res, p, parsedUrl)
566 567 568
          return {
            finished: true,
          }
569
        },
570
      },
J
JJ Kasper 已提交
571 572
      {
        match: route('/_next/data/:path*'),
573 574
        type: 'route',
        name: '_next/data catchall',
J
JJ Kasper 已提交
575
        fn: async (req, res, params, _parsedUrl) => {
J
JJ Kasper 已提交
576 577 578
          // 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) {
579 580 581 582
            await this.render404(req, res, _parsedUrl)
            return {
              finished: true,
            }
J
JJ Kasper 已提交
583 584 585 586 587 588
          }
          // 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')) {
589 590 591 592
            await this.render404(req, res, _parsedUrl)
            return {
              finished: true,
            }
J
JJ Kasper 已提交
593 594 595
          }

          // re-create page's pathname
596
          let pathname = `/${params.path.join('/')}`
597
          pathname = getRouteFromAssetPath(pathname, '.json')
598

J
Joe Haddad 已提交
599
          const { i18n } = this.nextConfig
600 601

          if (i18n) {
602 603 604
            const { host } = req?.headers || {}
            // remove port from host and remove port if present
            const hostname = host?.split(':')[0].toLowerCase()
605
            const localePathResult = normalizeLocalePath(pathname, i18n.locales)
606
            const { defaultLocale } =
607
              detectDomainLocale(i18n.domains, hostname) || {}
608
            let detectedLocale = defaultLocale
609 610 611 612 613

            if (localePathResult.detectedLocale) {
              pathname = localePathResult.pathname
              detectedLocale = localePathResult.detectedLocale
            }
614

615
            _parsedUrl.query.__nextLocale = detectedLocale!
616 617
            _parsedUrl.query.__nextDefaultLocale =
              defaultLocale || i18n.defaultLocale
618
          }
J
JJ Kasper 已提交
619

J
JJ Kasper 已提交
620
          const parsedUrl = parseUrl(pathname, true)
621

J
JJ Kasper 已提交
622 623 624 625
          await this.render(
            req,
            res,
            pathname,
626
            { ..._parsedUrl.query, _nextDataReq: '1' },
J
JJ Kasper 已提交
627 628
            parsedUrl
          )
629 630 631
          return {
            finished: true,
          }
J
JJ Kasper 已提交
632 633
        },
      },
S
Steven 已提交
634 635 636 637 638 639 640
      {
        match: route('/_next/image'),
        type: 'route',
        name: '_next/image catchall',
        fn: (req, res, _params, parsedUrl) =>
          imageOptimizer(server, req, res, parsedUrl),
      },
T
Tim Neutkens 已提交
641
      {
642
        match: route('/_next/:path*'),
643 644
        type: 'route',
        name: '_next catchall',
T
Tim Neutkens 已提交
645
        // This path is needed because `render()` does a check for `/_next` and the calls the routing again
646
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
647
          await this.render404(req, res, parsedUrl)
648 649 650
          return {
            finished: true,
          }
L
Lukáš Huvar 已提交
651 652
        },
      },
653 654
      ...publicRoutes,
      ...staticFilesRoute,
T
Tim Neutkens 已提交
655
    ]
656

657 658 659 660 661 662
    const getCustomRouteBasePath = (r: { basePath?: false }) => {
      return r.basePath !== false && this.renderOpts.dev
        ? this.nextConfig.basePath
        : ''
    }

663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690
    const getCustomRouteLocalePrefix = (r: {
      locale?: false
      destination?: string
    }) => {
      const { i18n } = this.nextConfig

      if (!i18n || r.locale === false || !this.renderOpts.dev) return ''

      if (r.destination && r.destination.startsWith('/')) {
        r.destination = `/:nextInternalLocale${r.destination}`
      }

      return `/:nextInternalLocale(${i18n.locales
        .map((locale: string) => escapeStringRegexp(locale))
        .join('|')})`
    }

    const getCustomRoute = (
      r: Rewrite | Redirect | Header,
      type: RouteType
    ) => {
      const match = getCustomRouteMatcher(
        `${getCustomRouteBasePath(r)}${getCustomRouteLocalePrefix(r)}${
          r.source
        }`
      )

      return {
691 692
        ...r,
        type,
693
        match,
694 695
        name: type,
        fn: async (_req, _res, _params, _parsedUrl) => ({ finished: false }),
696 697
      } as Route & Rewrite & Header
    }
698 699 700 701 702 703 704 705 706 707 708 709 710 711

    // 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) {
712 713
              key = compileNonPath(key, params)
              value = compileNonPath(value, params)
714
            }
715 716 717 718 719 720 721
            res.setHeader(key, value)
          }
          return { finished: false }
        },
      } as Route
    })

722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737
    // since initial query values are decoded by querystring.parse
    // we need to re-encode them here but still allow passing through
    // values from rewrites/redirects
    const stringifyQuery = (req: IncomingMessage, query: ParsedUrlQuery) => {
      const initialQueryValues = Object.values((req as any).__NEXT_INIT_QUERY)

      return stringifyQs(query, undefined, undefined, {
        encodeURIComponent(value) {
          if (initialQueryValues.some((val) => val === value)) {
            return encodeURIComponent(value)
          }
          return value
        },
      })
    }

738 739 740 741 742 743
    const redirects = this.customRoutes.redirects.map((redirect) => {
      const redirectRoute = getCustomRoute(redirect, 'redirect')
      return {
        type: redirectRoute.type,
        match: redirectRoute.match,
        statusCode: redirectRoute.statusCode,
744
        name: `Redirect route ${redirectRoute.source}`,
745
        fn: async (req, res, params, parsedUrl) => {
746 747 748
          const { parsedDestination } = prepareDestination(
            redirectRoute.destination,
            params,
749 750 751
            parsedUrl.query,
            false,
            getCustomRouteBasePath(redirectRoute)
752
          )
753 754

          const { query } = parsedDestination
755
          delete (parsedDestination as any).query
756

757
          parsedDestination.search = stringifyQuery(req, query)
758

759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780
          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 {
781
        ...rewriteRoute,
782 783
        check: true,
        type: rewriteRoute.type,
784
        name: `Rewrite route ${rewriteRoute.source}`,
785 786 787 788 789 790
        match: rewriteRoute.match,
        fn: async (req, res, params, parsedUrl) => {
          const { newUrl, parsedDestination } = prepareDestination(
            rewriteRoute.destination,
            params,
            parsedUrl.query,
791 792
            true,
            getCustomRouteBasePath(rewriteRoute)
793
          )
794

795 796
          // external rewrite, proxy it
          if (parsedDestination.protocol) {
797
            const { query } = parsedDestination
798
            delete (parsedDestination as any).query
799
            parsedDestination.search = stringifyQuery(req, query)
800

801 802 803 804 805 806 807 808 809 810 811
            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)
            })
812 813 814
            return {
              finished: true,
            }
815 816
          }
          ;(req as any)._nextRewroteUrl = newUrl
817 818
          ;(req as any)._nextDidRewrite =
            (req as any)._nextRewroteUrl !== req.url
819

820 821 822 823 824 825 826 827
          return {
            finished: false,
            pathname: newUrl,
            query: parsedDestination.query,
          }
        },
      } as Route
    })
828 829 830 831 832 833

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

J
Jan Potoms 已提交
839
        // next.js core assumes page path without trailing slash
840
        pathname = removePathTrailingSlash(pathname)
J
Jan Potoms 已提交
841

842 843 844 845 846 847 848 849 850 851 852 853
        if (this.nextConfig.i18n) {
          const localePathResult = normalizeLocalePath(
            pathname,
            this.nextConfig.i18n?.locales
          )

          if (localePathResult.detectedLocale) {
            pathname = localePathResult.pathname
            parsedUrl.query.__nextLocale = localePathResult.detectedLocale
          }
        }

854
        if (params?.path?.[0] === 'api') {
855 856 857
          const handled = await this.handleApiRequest(
            req as NextApiRequest,
            res as NextApiResponse,
858
            pathname,
859
            query
860 861 862 863 864 865 866
          )
          if (handled) {
            return { finished: true }
          }
        }

        await this.render(req, res, pathname, query, parsedUrl)
867 868 869 870
        return {
          finished: true,
        }
      },
871
    }
872

873
    const { useFileSystemPublicRoutes } = this.nextConfig
J
Joe Haddad 已提交
874

875 876
    if (useFileSystemPublicRoutes) {
      this.dynamicRoutes = this.getDynamicRoutes()
877
    }
N
nkzawa 已提交
878

879
    return {
880
      headers,
881
      fsRoutes,
882 883
      rewrites,
      redirects,
884
      catchAllRoute,
885
      useFileSystemPublicRoutes,
886
      dynamicRoutes: this.dynamicRoutes,
887
      basePath: this.nextConfig.basePath,
888
      pageChecker: this.hasPage.bind(this),
889
      locales: this.nextConfig.i18n?.locales,
890
    }
T
Tim Neutkens 已提交
891 892
  }

893
  private async getPagePath(pathname: string): Promise<string> {
894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910
    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
  }

911 912 913 914 915
  protected async _beforeCatchAllRender(
    _req: IncomingMessage,
    _res: ServerResponse,
    _params: Params,
    _parsedUrl: UrlWithParsedQuery
916
  ): Promise<boolean> {
917 918 919
    return false
  }

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

L
Lukáš Huvar 已提交
923 924 925 926 927 928
  /**
   * Resolves `API` request, in development builds on demand
   * @param req http request
   * @param res http response
   * @param pathname path of request
   */
J
Joe Haddad 已提交
929
  private async handleApiRequest(
930 931
    req: IncomingMessage,
    res: ServerResponse,
932 933
    pathname: string,
    query: ParsedUrlQuery
934
  ): Promise<boolean> {
935
    let page = pathname
L
Lukáš Huvar 已提交
936
    let params: Params | boolean = false
937
    let pageFound = await this.hasPage(page)
J
JJ Kasper 已提交
938

939
    if (!pageFound && this.dynamicRoutes) {
L
Lukáš Huvar 已提交
940 941
      for (const dynamicRoute of this.dynamicRoutes) {
        params = dynamicRoute.match(pathname)
942
        if (dynamicRoute.page.startsWith('/api') && params) {
943 944
          page = dynamicRoute.page
          pageFound = true
L
Lukáš Huvar 已提交
945 946 947 948 949
          break
        }
      }
    }

950
    if (!pageFound) {
951
      return false
J
JJ Kasper 已提交
952
    }
953 954 955 956
    // Make sure the page is built before getting the path
    // or else it won't be in the manifest yet
    await this.ensureApiPage(page)

957 958 959 960 961 962 963 964 965 966
    let builtPagePath
    try {
      builtPagePath = await this.getPagePath(page)
    } catch (err) {
      if (err.code === 'ENOENT') {
        return false
      }
      throw err
    }

967
    const pageModule = await require(builtPagePath)
968
    query = { ...query, ...params }
J
JJ Kasper 已提交
969

970
    if (!this.renderOpts.dev && this._isLikeServerless) {
971
      if (typeof pageModule.default === 'function') {
972
        prepareServerlessUrl(req, query)
973 974
        await pageModule.default(req, res)
        return true
J
JJ Kasper 已提交
975 976 977
      }
    }

J
Joe Haddad 已提交
978 979 980 981 982
    await apiResolver(
      req,
      res,
      query,
      pageModule,
983
      this.renderOpts.previewProps,
984
      false,
J
Joe Haddad 已提交
985 986
      this.onErrorMiddleware
    )
987
    return true
L
Lukáš Huvar 已提交
988 989
  }

990
  protected generatePublicRoutes(): Route[] {
991
    const publicFiles = new Set(
992 993 994
      recursiveReadDirSync(this.publicDir).map((p) =>
        encodeURI(p.replace(/\\/g, '/'))
      )
995 996 997 998 999 1000 1001
    )

    return [
      {
        match: route('/:path*'),
        name: 'public folder catchall',
        fn: async (req, res, params, parsedUrl) => {
1002
          const pathParts: string[] = params.path || []
1003 1004 1005 1006
          const { basePath } = this.nextConfig

          // if basePath is defined require it be present
          if (basePath) {
1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019
            const basePathParts = basePath.split('/')
            // remove first empty value
            basePathParts.shift()

            if (
              !basePathParts.every((part: string, idx: number) => {
                return part === pathParts[idx]
              })
            ) {
              return { finished: false }
            }

            pathParts.splice(0, basePathParts.length)
1020 1021
          }

1022
          const path = `/${pathParts.join('/')}`
1023 1024 1025 1026 1027

          if (publicFiles.has(path)) {
            await this.serveStatic(
              req,
              res,
1028
              join(this.publicDir, ...pathParts),
1029 1030
              parsedUrl
            )
1031 1032 1033
            return {
              finished: true,
            }
1034 1035 1036 1037 1038 1039 1040
          }
          return {
            finished: false,
          }
        },
      } as Route,
    ]
1041 1042
  }

1043 1044 1045
  protected getDynamicRoutes(): Array<DynamicRouteItem> {
    const addedPages = new Set<string>()

1046 1047
    return getSortedRoutes(Object.keys(this.pagesManifest!))
      .filter(isDynamicRoute)
1048 1049 1050 1051 1052 1053 1054 1055 1056 1057
      .map((page) => {
        page = normalizeLocalePath(page, this.nextConfig.i18n?.locales).pathname
        if (addedPages.has(page)) return null
        addedPages.add(page)
        return {
          page,
          match: getRouteMatcher(getRouteRegex(page)),
        }
      })
      .filter((item): item is DynamicRouteItem => Boolean(item))
J
Joe Haddad 已提交
1058 1059
  }

1060
  private handleCompression(req: IncomingMessage, res: ServerResponse): void {
1061 1062 1063 1064 1065
    if (this.compression) {
      this.compression(req, res, () => {})
    }
  }

1066
  protected async run(
J
Joe Haddad 已提交
1067 1068
    req: IncomingMessage,
    res: ServerResponse,
1069
    parsedUrl: UrlWithParsedQuery
1070
  ): Promise<void> {
1071 1072
    this.handleCompression(req, res)

1073
    try {
1074 1075
      const matched = await this.router.execute(req, res, parsedUrl)
      if (matched) {
1076 1077 1078 1079 1080 1081 1082 1083
        return
      }
    } catch (err) {
      if (err.code === 'DECODE_FAILED') {
        res.statusCode = 400
        return this.renderError(null, req, res, '/_error', {})
      }
      throw err
1084 1085
    }

1086
    await this.render404(req, res, parsedUrl)
N
nkzawa 已提交
1087 1088
  }

1089
  protected async sendHTML(
J
Joe Haddad 已提交
1090 1091
    req: IncomingMessage,
    res: ServerResponse,
1092
    html: string
1093
  ): Promise<void> {
T
Tim Neutkens 已提交
1094
    const { generateEtags, poweredByHeader } = this.renderOpts
1095 1096 1097 1098
    return sendPayload(req, res, html, 'html', {
      generateEtags,
      poweredByHeader,
    })
1099 1100
  }

J
Joe Haddad 已提交
1101 1102 1103 1104 1105
  public async render(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
1106
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1107
  ): Promise<void> {
1108 1109 1110 1111 1112 1113
    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`
      )
    }

1114 1115 1116 1117 1118 1119 1120 1121 1122 1123
    if (
      this.renderOpts.customServer &&
      pathname === '/index' &&
      !(await this.hasPage('/index'))
    ) {
      // maintain backwards compatibility for custom server
      // (see custom-server integration tests)
      pathname = '/'
    }

1124
    const url: any = req.url
1125

1126 1127 1128 1129
    // 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
1130
    if (
1131 1132 1133
      !query._nextDataReq &&
      (url.match(/^\/_next\//) ||
        (this.hasStaticDir && url.match(/^\/static\//)))
1134
    ) {
1135 1136 1137
      return this.handleRequest(req, res, parsedUrl)
    }

1138
    if (isBlockedPage(pathname)) {
1139
      return this.render404(req, res, parsedUrl)
1140 1141
    }

1142
    const html = await this.renderToHTML(req, res, pathname, query)
1143 1144
    // Request was ended by the user
    if (html === null) {
1145 1146 1147
      return
    }

1148
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
1149
  }
N
nkzawa 已提交
1150

J
Joe Haddad 已提交
1151
  private async findPageComponents(
J
Joe Haddad 已提交
1152
    pathname: string,
1153 1154 1155
    query: ParsedUrlQuery = {},
    params: Params | null = null
  ): Promise<FindComponentsResult | null> {
1156
    let paths = [
1157 1158 1159 1160
      // try serving a static AMP version first
      query.amp ? normalizePagePath(pathname) + '.amp' : null,
      pathname,
    ].filter(Boolean)
1161 1162 1163 1164 1165 1166 1167 1168 1169 1170

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

1171
    for (const pagePath of paths) {
J
JJ Kasper 已提交
1172
      try {
1173
        const components = await loadComponents(
J
Joe Haddad 已提交
1174
          this.distDir,
1175 1176
          pagePath!,
          !this.renderOpts.dev && this._isLikeServerless
J
Joe Haddad 已提交
1177
        )
1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189
        // 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
        }

1190 1191 1192
        return {
          components,
          query: {
1193
            ...(components.getStaticProps
1194 1195 1196 1197
              ? {
                  amp: query.amp,
                  _nextDataReq: query._nextDataReq,
                  __nextLocale: query.__nextLocale,
1198
                  __nextDefaultLocale: query.__nextDefaultLocale,
1199
                }
1200 1201 1202 1203
              : query),
            ...(params || {}),
          },
        }
J
JJ Kasper 已提交
1204 1205 1206 1207
      } catch (err) {
        if (err.code !== 'ENOENT') throw err
      }
    }
1208
    return null
J
Joe Haddad 已提交
1209 1210
  }

1211
  protected async getStaticPaths(
1212 1213 1214
    pathname: string
  ): Promise<{
    staticPaths: string[] | undefined
1215
    fallbackMode: 'static' | 'blocking' | false
1216
  }> {
1217 1218 1219 1220 1221
    // `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.
1222 1223
    const fallbackField = this.getPrerenderManifest().dynamicRoutes[pathname]
      .fallback
1224

1225 1226 1227 1228 1229 1230 1231 1232 1233
    return {
      staticPaths,
      fallbackMode:
        typeof fallbackField === 'string'
          ? 'static'
          : fallbackField === null
          ? 'blocking'
          : false,
    }
1234 1235
  }

J
Joe Haddad 已提交
1236 1237 1238 1239
  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1240
    { components, query }: FindComponentsResult,
1241
    opts: RenderOptsPartial
1242
  ): Promise<string | null> {
1243 1244
    const is404Page = pathname === '/404'

1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255
    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

1256
    // we need to ensure the status code if /404 is visited directly
1257
    if (is404Page && !isDataReq) {
1258 1259 1260
      res.statusCode = 404
    }

J
JJ Kasper 已提交
1261
    // handle static page
1262 1263
    if (typeof components.Component === 'string') {
      return components.Component
J
Joe Haddad 已提交
1264 1265
    }

1266 1267 1268 1269
    if (!query.amp) {
      delete query.amp
    }

J
JJ Kasper 已提交
1270
    const locale = query.__nextLocale as string
1271 1272 1273 1274
    const defaultLocale = isSSG
      ? this.nextConfig.i18n?.defaultLocale
      : (query.__nextDefaultLocale as string)

J
JJ Kasper 已提交
1275
    delete query.__nextLocale
1276
    delete query.__nextDefaultLocale
1277

J
Joe Haddad 已提交
1278
    const { i18n } = this.nextConfig
1279
    const locales = i18n.locales as string[]
J
JJ Kasper 已提交
1280

1281 1282 1283 1284 1285 1286 1287 1288
    let previewData: string | false | object | undefined
    let isPreviewMode = false

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

1289 1290 1291
    // 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
1292 1293 1294
    let urlPathname = parseUrl(req.url || '').pathname || '/'

    let resolvedUrlPathname = (req as any)._nextRewroteUrl
1295
      ? (req as any)._nextRewroteUrl
1296
      : urlPathname
1297

1298 1299 1300 1301 1302 1303 1304 1305 1306
    resolvedUrlPathname = removePathTrailingSlash(resolvedUrlPathname)
    urlPathname = removePathTrailingSlash(urlPathname)

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

J
Joe Haddad 已提交
1308
      if (this.nextConfig.i18n) {
J
JJ Kasper 已提交
1309
        return normalizeLocalePath(path, locales).pathname
1310
      }
1311 1312
      return path
    }
1313

1314 1315 1316 1317
    const handleRedirect = (pageData: any) => {
      const redirect = {
        destination: pageData.pageProps.__N_REDIRECT,
        statusCode: pageData.pageProps.__N_REDIRECT_STATUS,
1318
        basePath: pageData.pageProps.__N_REDIRECT_BASE_PATH,
1319 1320
      }
      const statusCode = getRedirectStatus(redirect)
1321 1322 1323 1324 1325
      const { basePath } = this.nextConfig

      if (basePath && redirect.basePath !== false) {
        redirect.destination = `${basePath}${redirect.destination}`
      }
1326 1327 1328 1329 1330 1331 1332 1333 1334 1335

      if (statusCode === PERMANENT_REDIRECT_STATUS) {
        res.setHeader('Refresh', `0;url=${redirect.destination}`)
      }

      res.statusCode = statusCode
      res.setHeader('Location', redirect.destination)
      res.end()
    }

1336 1337
    // remove /_next/data prefix from urlPathname so it matches
    // for direct page visit and /_next/data visit
1338 1339 1340
    if (isDataReq) {
      resolvedUrlPathname = stripNextDataPath(resolvedUrlPathname)
      urlPathname = stripNextDataPath(urlPathname)
1341 1342
    }

1343
    let ssgCacheKey =
1344 1345
      isPreviewMode || !isSSG
        ? undefined // Preview mode bypasses the cache
1346
        : `${locale ? `/${locale}` : ''}${
1347 1348 1349
            (pathname === '/' || resolvedUrlPathname === '/') && locale
              ? ''
              : resolvedUrlPathname
1350
          }${query.amp ? '.amp' : ''}`
J
JJ Kasper 已提交
1351

1352 1353 1354 1355 1356 1357
    if (is404Page && isSSG) {
      ssgCacheKey = `${locale ? `/${locale}` : ''}${pathname}${
        query.amp ? '.amp' : ''
      }`
    }

J
JJ Kasper 已提交
1358
    // Complete the response with cached data if its present
1359 1360 1361
    const cachedData = ssgCacheKey
      ? await this.incrementalCache.get(ssgCacheKey)
      : undefined
1362

J
JJ Kasper 已提交
1363
    if (cachedData) {
1364 1365 1366 1367 1368 1369
      if (cachedData.isNotFound) {
        // we don't currently revalidate when notFound is returned
        // so trigger rendering 404 here
        throw new NoFallbackError()
      }

1370
      const data = isDataReq
J
JJ Kasper 已提交
1371 1372 1373
        ? JSON.stringify(cachedData.pageData)
        : cachedData.html

1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397
      if (!isDataReq && cachedData.pageData?.pageProps?.__N_REDIRECT) {
        await handleRedirect(cachedData.pageData)
      } else {
        sendPayload(
          req,
          res,
          data,
          isDataReq ? 'json' : 'html',
          {
            generateEtags: this.renderOpts.generateEtags,
            poweredByHeader: this.renderOpts.poweredByHeader,
          },
          !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,
              }
            : undefined
        )
      }
J
JJ Kasper 已提交
1398 1399 1400 1401 1402

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

J
JJ Kasper 已提交
1405
    // If we're here, that means data is missing or it's stale.
1406
    const maybeCoalesceInvoke = ssgCacheKey
1407
      ? (fn: any) => withCoalescedInvoke(fn).bind(null, ssgCacheKey!, [])
1408 1409 1410 1411
      : (fn: any) => async () => {
          const value = await fn()
          return { isOrigin: true, value }
        }
J
JJ Kasper 已提交
1412

1413 1414 1415 1416 1417
    const doRender = maybeCoalesceInvoke(
      async (): Promise<{
        html: string | null
        pageData: any
        sprRevalidate: number | false
1418
        isNotFound?: boolean
1419
        isRedirect?: boolean
1420 1421 1422 1423
      }> => {
        let pageData: any
        let html: string | null
        let sprRevalidate: number | false
1424
        let isNotFound: boolean | undefined
1425
        let isRedirect: boolean | undefined
1426 1427 1428 1429 1430 1431 1432

        let renderResult
        // handle serverless
        if (isLikeServerless) {
          renderResult = await (components.Component as any).renderReqToHTML(
            req,
            res,
P
Prateek Bhatnagar 已提交
1433 1434 1435
            'passthrough',
            {
              fontManifest: this.renderOpts.fontManifest,
1436
              locale,
1437
              locales,
1438
              defaultLocale,
P
Prateek Bhatnagar 已提交
1439
            }
1440
          )
J
JJ Kasper 已提交
1441

1442 1443 1444
          html = renderResult.html
          pageData = renderResult.renderOpts.pageData
          sprRevalidate = renderResult.renderOpts.revalidate
1445
          isNotFound = renderResult.renderOpts.isNotFound
1446
          isRedirect = renderResult.renderOpts.isRedirect
1447
        } else {
1448 1449 1450 1451 1452 1453 1454
          const origQuery = parseUrl(req.url || '', true).query
          const resolvedUrl = formatUrl({
            pathname: resolvedUrlPathname,
            // make sure to only add query values from original URL
            query: origQuery,
          })

1455 1456 1457 1458
          const renderOpts: RenderOpts = {
            ...components,
            ...opts,
            isDataReq,
1459
            resolvedUrl,
1460
            locale,
1461
            locales,
1462
            defaultLocale,
1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473
            // 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,
1474
          }
1475

1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487
          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
1488
          isNotFound = (renderOpts as any).isNotFound
1489
          isRedirect = (renderOpts as any).isRedirect
J
JJ Kasper 已提交
1490 1491
        }

1492
        return { html, pageData, sprRevalidate, isNotFound, isRedirect }
J
JJ Kasper 已提交
1493
      }
1494
    )
J
JJ Kasper 已提交
1495

1496
    const isProduction = !this.renderOpts.dev
J
Joe Haddad 已提交
1497
    const isDynamicPathname = isDynamicRoute(pathname)
1498
    const didRespond = isResSent(res)
1499

1500
    const { staticPaths, fallbackMode } = hasStaticPaths
1501
      ? await this.getStaticPaths(pathname)
1502
      : { staticPaths: undefined, fallbackMode: false }
1503

1504 1505 1506 1507 1508
    // When we did not respond from cache, we need to choose to block on
    // rendering or return a skeleton.
    //
    // * Data requests always block.
    //
1509 1510
    // * Blocking mode fallback always blocks.
    //
1511 1512
    // * Preview mode toggles all pages to be resolved in a blocking manner.
    //
1513
    // * Non-dynamic pages should block (though this is an impossible
1514 1515
    //   case in production).
    //
1516 1517
    // * Dynamic pages should return their skeleton if not defined in
    //   getStaticPaths, then finish the data request on the client-side.
1518
    //
J
Joe Haddad 已提交
1519
    if (
1520
      fallbackMode !== 'blocking' &&
1521
      ssgCacheKey &&
1522 1523 1524
      !didRespond &&
      !isPreviewMode &&
      isDynamicPathname &&
1525 1526
      // Development should trigger fallback when the path is not in
      // `getStaticPaths`
1527 1528
      (isProduction ||
        !staticPaths ||
1529 1530 1531 1532 1533
        // static paths always includes locale so make sure it's prefixed
        // with it
        !staticPaths.includes(
          `${locale ? '/' + locale : ''}${resolvedUrlPathname}`
        ))
J
Joe Haddad 已提交
1534
    ) {
1535 1536 1537 1538 1539
      if (
        // In development, fall through to render to handle missing
        // getStaticPaths.
        (isProduction || staticPaths) &&
        // When fallback isn't present, abort this render so we 404
1540
        fallbackMode !== 'static'
1541
      ) {
1542
        throw new NoFallbackError()
1543 1544
      }

1545 1546
      if (!isDataReq) {
        let html: string
1547

1548 1549
        // Production already emitted the fallback as static HTML.
        if (isProduction) {
1550 1551 1552
          html = await this.incrementalCache.getFallback(
            locale ? `/${locale}${pathname}` : pathname
          )
1553 1554 1555 1556 1557 1558 1559 1560 1561
        }
        // 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
1562 1563
        }

1564 1565 1566 1567 1568 1569
        sendPayload(req, res, html, 'html', {
          generateEtags: this.renderOpts.generateEtags,
          poweredByHeader: this.renderOpts.poweredByHeader,
        })
        return null
      }
1570 1571
    }

1572 1573
    const {
      isOrigin,
1574
      value: { html, pageData, sprRevalidate, isNotFound, isRedirect },
1575
    } = await doRender()
1576
    let resHtml = html
1577 1578 1579 1580 1581 1582

    if (
      !isResSent(res) &&
      !isNotFound &&
      (isSSG || isDataReq || isServerProps)
    ) {
1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603
      if (isRedirect && !isDataReq) {
        await handleRedirect(pageData)
      } else {
        sendPayload(
          req,
          res,
          isDataReq ? JSON.stringify(pageData) : html,
          isDataReq ? 'json' : 'html',
          {
            generateEtags: this.renderOpts.generateEtags,
            poweredByHeader: this.renderOpts.poweredByHeader,
          },
          !this.renderOpts.dev || (isServerProps && !isDataReq)
            ? {
                private: isPreviewMode,
                stateful: !isSSG,
                revalidate: sprRevalidate,
              }
            : undefined
        )
      }
1604
      resHtml = null
1605
    }
J
JJ Kasper 已提交
1606

1607
    // Update the cache if the head request and cacheable
1608
    if (isOrigin && ssgCacheKey) {
1609 1610
      await this.incrementalCache.set(
        ssgCacheKey,
1611
        { html: html!, pageData, isNotFound, isRedirect },
1612 1613
        sprRevalidate
      )
1614 1615
    }

1616 1617 1618
    if (isNotFound) {
      throw new NoFallbackError()
    }
1619
    return resHtml
1620 1621
  }

1622
  public async renderToHTML(
J
Joe Haddad 已提交
1623 1624 1625
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1626
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1627
  ): Promise<string | null> {
1628 1629 1630
    try {
      const result = await this.findPageComponents(pathname, query)
      if (result) {
1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642
        try {
          return await this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
            result,
            { ...this.renderOpts }
          )
        } catch (err) {
          if (!(err instanceof NoFallbackError)) {
            throw err
          }
1643
        }
1644
      }
J
Joe Haddad 已提交
1645

1646 1647 1648 1649 1650 1651
      if (this.dynamicRoutes) {
        for (const dynamicRoute of this.dynamicRoutes) {
          const params = dynamicRoute.match(pathname)
          if (!params) {
            continue
          }
J
Joe Haddad 已提交
1652

1653
          const dynamicRouteResult = await this.findPageComponents(
1654 1655 1656 1657
            dynamicRoute.page,
            query,
            params
          )
1658
          if (dynamicRouteResult) {
1659 1660 1661 1662 1663
            try {
              return await this.renderToHTMLWithComponents(
                req,
                res,
                dynamicRoute.page,
1664
                dynamicRouteResult,
1665 1666 1667 1668 1669 1670
                { ...this.renderOpts, params }
              )
            } catch (err) {
              if (!(err instanceof NoFallbackError)) {
                throw err
              }
1671
            }
J
Joe Haddad 已提交
1672 1673
          }
        }
1674 1675 1676
      }
    } catch (err) {
      this.logError(err)
1677 1678 1679 1680 1681

      if (err && err.code === 'DECODE_FAILED') {
        res.statusCode = 400
        return await this.renderErrorToHTML(err, req, res, pathname, query)
      }
1682 1683 1684 1685 1686
      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 已提交
1687 1688
  }

J
Joe Haddad 已提交
1689 1690 1691 1692 1693
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1694
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1695 1696 1697
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
1698
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
1699
    )
N
Naoyuki Kanezawa 已提交
1700
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
1701
    if (html === null) {
1702 1703
      return
    }
1704
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
1705 1706
  }

1707 1708 1709 1710 1711 1712 1713 1714 1715
  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 已提交
1716 1717 1718 1719 1720
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
1721
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1722
  ) {
1723
    let result: null | FindComponentsResult = null
1724

1725 1726 1727
    const is404 = res.statusCode === 404
    let using404Page = false

1728
    // use static 404 page if available and is 404 response
1729
    if (is404) {
1730
      result = await this.findPageComponents('/404', query)
1731
      using404Page = result !== null
1732 1733 1734 1735 1736 1737
    }

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

1738 1739 1740
    if (
      process.env.NODE_ENV !== 'production' &&
      !using404Page &&
1741 1742
      (await this.hasPage('/_error')) &&
      !(await this.hasPage('/404'))
1743 1744 1745 1746
    ) {
      this.customErrorNo404Warn()
    }

1747
    let html: string | null
1748
    try {
1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759
      try {
        html = await this.renderToHTMLWithComponents(
          req,
          res,
          using404Page ? '/404' : '/_error',
          result!,
          {
            ...this.renderOpts,
            err,
          }
        )
1760 1761
      } catch (maybeFallbackError) {
        if (maybeFallbackError instanceof NoFallbackError) {
1762
          throw new Error('invariant: failed to render error page')
1763
        }
1764
        throw maybeFallbackError
1765
      }
1766 1767
    } catch (renderToHtmlError) {
      console.error(renderToHtmlError)
1768 1769 1770 1771
      res.statusCode = 500
      html = 'Internal Server Error'
    }
    return html
N
Naoyuki Kanezawa 已提交
1772 1773
  }

J
Joe Haddad 已提交
1774 1775 1776
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
1777
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1778
  ): Promise<void> {
1779 1780
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
N
Naoyuki Kanezawa 已提交
1781
    res.statusCode = 404
1782
    return this.renderError(null, req, res, pathname!, query)
N
Naoyuki Kanezawa 已提交
1783
  }
N
Naoyuki Kanezawa 已提交
1784

J
Joe Haddad 已提交
1785 1786 1787 1788
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
1789
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1790
  ): Promise<void> {
A
Arunoda Susiripala 已提交
1791
    if (!this.isServeableUrl(path)) {
1792
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
1793 1794
    }

1795 1796 1797 1798 1799 1800
    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 已提交
1801
    try {
1802
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
1803
    } catch (err) {
T
Tim Neutkens 已提交
1804
      if (err.code === 'ENOENT' || err.statusCode === 404) {
1805
        this.render404(req, res, parsedUrl)
1806 1807 1808
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
1809 1810 1811 1812 1813 1814
      } else {
        throw err
      }
    }
  }

1815 1816 1817 1818 1819 1820 1821 1822 1823
  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 已提交
1824
      userFilesStatic = recursiveReadDirSync(pathUserFilesStatic).map((f) =>
1825 1826 1827 1828 1829 1830
        join('.', 'static', f)
      )
    }

    let userFilesPublic: string[] = []
    if (this.publicDir && fs.existsSync(this.publicDir)) {
J
Joe Haddad 已提交
1831
      userFilesPublic = recursiveReadDirSync(this.publicDir).map((f) =>
1832 1833 1834 1835 1836 1837 1838
        join('.', 'public', f)
      )
    }

    let nextFilesStatic: string[] = []
    nextFilesStatic = recursiveReadDirSync(
      join(this.distDir, 'static')
J
Joe Haddad 已提交
1839
    ).map((f) => join('.', relative(this.dir, this.distDir), 'static', f))
1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873

    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 已提交
1874
    if (
1875 1876 1877
      (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 已提交
1878 1879 1880 1881
    ) {
      return false
    }

1882 1883 1884 1885
    // Check against the real filesystem paths
    const filesystemUrls = this.getFilesystemPaths()
    const resolved = relative(this.dir, untrustedFilePath)
    return filesystemUrls.has(resolved)
A
Arunoda Susiripala 已提交
1886 1887
  }

1888
  protected readBuildId(): string {
1889 1890 1891 1892 1893
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
1894
        throw new Error(
1895
          `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 已提交
1896
        )
1897 1898 1899
      }

      throw err
1900
    }
1901
  }
1902

1903
  protected get _isLikeServerless(): boolean {
1904 1905
    return isTargetLikeServerless(this.nextConfig.target)
  }
1906
}
1907

1908 1909 1910 1911
function prepareServerlessUrl(
  req: IncomingMessage,
  query: ParsedUrlQuery
): void {
1912 1913 1914 1915 1916 1917 1918 1919 1920 1921
  const curUrl = parseUrl(req.url!, true)
  req.url = formatUrl({
    ...curUrl,
    search: undefined,
    query: {
      ...curUrl.query,
      ...query,
    },
  })
}
1922 1923

class NoFallbackError extends Error {}