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

const getCustomRouteMatcher = pathMatch(true)
91 92 93

type NextConfig = any

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

345
      let localeDomainRedirect: string | undefined
346 347 348 349 350
      ;(req as any).__nextHadTrailingSlash = pathname!.endsWith('/')

      if (pathname === '/') {
        ;(req as any).__nextHadTrailingSlash = this.nextConfig.trailingSlash
      }
351 352 353 354 355 356 357 358
      const localePathResult = normalizeLocalePath(pathname!, i18n.locales)

      if (localePathResult.detectedLocale) {
        detectedLocale = localePathResult.detectedLocale
        req.url = formatUrl({
          ...parsed,
          pathname: localePathResult.pathname,
        })
J
JJ Kasper 已提交
359
        ;(req as any).__nextStrippedLocale = true
360 361 362 363 364 365
        parsedUrl.pathname = `${basePath || ''}${localePathResult.pathname}${
          (req as any).__nextHadTrailingSlash &&
          localePathResult.pathname !== '/'
            ? '/'
            : ''
        }`
366 367 368 369 370
      }

      // 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
371
      if (detectedDomain && pathname === '/') {
372 373 374 375
        const localeToCheck = acceptPreferredLocale
        // const localeToCheck = localePathResult.detectedLocale
        //   ? detectedLocale
        //   : acceptPreferredLocale
376

377 378 379
        const matchedDomain = detectDomainLocale(
          i18n.domains,
          undefined,
380
          localeToCheck
381 382
        )

383 384 385 386 387
        if (
          matchedDomain &&
          (matchedDomain.domain !== detectedDomain.domain ||
            localeToCheck !== matchedDomain.defaultLocale)
        ) {
388 389
          localeDomainRedirect = `http${matchedDomain.http ? '' : 's'}://${
            matchedDomain.domain
390 391
          }/${
            localeToCheck === matchedDomain.defaultLocale ? '' : localeToCheck
392
          }`
393 394 395
        }
      }

396
      const denormalizedPagePath = denormalizePagePath(pathname || '/')
397
      const detectedDefaultLocale =
398 399
        !detectedLocale ||
        detectedLocale.toLowerCase() === defaultLocale.toLowerCase()
400 401 402 403
      const shouldStripDefaultLocale = false
      // detectedDefaultLocale &&
      // denormalizedPagePath.toLowerCase() ===
      //   `/${i18n.defaultLocale.toLowerCase()}`
404

405 406
      const shouldAddLocalePrefix =
        !detectedDefaultLocale && denormalizedPagePath === '/'
407

408
      detectedLocale = detectedLocale || i18n.defaultLocale
409

410 411
      if (
        i18n.localeDetection !== false &&
412 413 414
        (localeDomainRedirect ||
          shouldAddLocalePrefix ||
          shouldStripDefaultLocale)
415
      ) {
416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437
        // 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: '/',
            }),
          ])
        }

438 439
        res.setHeader(
          'Location',
440 441 442 443 444 445 446 447 448
          localeDomainRedirect
            ? localeDomainRedirect
            : formatUrl({
                // make sure to include any query values when redirecting
                ...parsed,
                pathname: shouldStripDefaultLocale
                  ? basePath || `/`
                  : `${basePath || ''}/${detectedLocale}`,
              })
449
        )
450
        res.statusCode = TEMPORARY_REDIRECT_STATUS
451
        res.end()
452
        return
453
      }
454

455 456 457
      parsedUrl.query.__nextDefaultLocale =
        detectedDomain?.defaultLocale || i18n.defaultLocale

458 459 460 461
      parsedUrl.query.__nextLocale =
        localePathResult.detectedLocale ||
        detectedDomain?.defaultLocale ||
        defaultLocale
462 463
    }

464
    res.statusCode = 200
465 466 467
    try {
      return await this.run(req, res, parsedUrl)
    } catch (err) {
J
Joe Haddad 已提交
468 469 470
      this.logError(err)
      res.statusCode = 500
      res.end('Internal Server Error')
471
    }
472 473
  }

474
  public getRequestHandler() {
475
    return this.handleRequest.bind(this)
N
nkzawa 已提交
476 477
  }

478
  public setAssetPrefix(prefix?: string): void {
479
    this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
480 481
  }

482
  // Backwards compatibility
483
  public async prepare(): Promise<void> {}
N
nkzawa 已提交
484

T
Tim Neutkens 已提交
485
  // Backwards compatibility
486
  protected async close(): Promise<void> {}
T
Tim Neutkens 已提交
487

488
  protected setImmutableAssetCacheControl(res: ServerResponse): void {
T
Tim Neutkens 已提交
489
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
N
nkzawa 已提交
490 491
  }

492
  protected getCustomRoutes(): CustomRoutes {
J
JJ Kasper 已提交
493 494 495
    return require(join(this.distDir, ROUTES_MANIFEST))
  }

496 497 498 499
  private _cachedPreviewManifest: PrerenderManifest | undefined
  protected getPrerenderManifest(): PrerenderManifest {
    if (this._cachedPreviewManifest) {
      return this._cachedPreviewManifest
J
Joe Haddad 已提交
500
    }
501 502 503 504 505 506
    const manifest = require(join(this.distDir, PRERENDER_MANIFEST))
    return (this._cachedPreviewManifest = manifest)
  }

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

509
  protected generateRoutes(): {
510
    basePath: string
511 512
    headers: Route[]
    rewrites: Route[]
513
    fsRoutes: Route[]
514
    redirects: Route[]
515 516
    catchAllRoute: Route
    pageChecker: PageChecker
517
    useFileSystemPublicRoutes: boolean
518
    dynamicRoutes: DynamicRoutes | undefined
519
    locales: string[]
520
  } {
S
Steven 已提交
521
    const server: Server = this
522 523 524
    const publicRoutes = fs.existsSync(this.publicDir)
      ? this.generatePublicRoutes()
      : []
J
JJ Kasper 已提交
525

526
    const staticFilesRoute = this.hasStaticDir
527 528 529 530 531
      ? [
          {
            // 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.
532
            // See more: https://github.com/vercel/next.js/issues/2617
533
            match: route('/static/:path*'),
534
            name: 'static catchall',
535
            fn: async (req, res, params, parsedUrl) => {
536
              const p = join(this.dir, 'static', ...params.path)
537
              await this.serveStatic(req, res, p, parsedUrl)
538 539 540
              return {
                finished: true,
              }
541 542 543 544
            },
          } as Route,
        ]
      : []
545

546
    const fsRoutes: Route[] = [
T
Tim Neutkens 已提交
547
      {
548
        match: route('/_next/static/:path*'),
549 550
        type: 'route',
        name: '_next/static catchall',
551
        fn: async (req, res, params, parsedUrl) => {
552
          // make sure to 404 for /_next/static itself
553 554 555 556 557 558
          if (!params.path) {
            await this.render404(req, res, parsedUrl)
            return {
              finished: true,
            }
          }
559

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

          // re-create page's pathname
607
          let pathname = `/${params.path.join('/')}`
608
          pathname = getRouteFromAssetPath(pathname, '.json')
609

J
Joe Haddad 已提交
610
          const { i18n } = this.nextConfig
611 612

          if (i18n) {
613 614 615
            const { host } = req?.headers || {}
            // remove port from host and remove port if present
            const hostname = host?.split(':')[0].toLowerCase()
616
            const localePathResult = normalizeLocalePath(pathname, i18n.locales)
617
            const { defaultLocale } =
618
              detectDomainLocale(i18n.domains, hostname) || {}
619 620

            let detectedLocale = ''
621 622 623 624 625

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

627
            _parsedUrl.query.__nextLocale = detectedLocale!
628 629
            _parsedUrl.query.__nextDefaultLocale =
              defaultLocale || i18n.defaultLocale
630 631 632 633 634 635 636

            if (!detectedLocale) {
              _parsedUrl.query.__nextLocale =
                _parsedUrl.query.__nextDefaultLocale
              await this.render404(req, res, _parsedUrl)
              return { finished: true }
            }
637
          }
J
JJ Kasper 已提交
638

J
JJ Kasper 已提交
639
          const parsedUrl = parseUrl(pathname, true)
640

J
JJ Kasper 已提交
641 642 643 644
          await this.render(
            req,
            res,
            pathname,
645
            { ..._parsedUrl.query, _nextDataReq: '1' },
J
JJ Kasper 已提交
646 647
            parsedUrl
          )
648 649 650
          return {
            finished: true,
          }
J
JJ Kasper 已提交
651 652
        },
      },
S
Steven 已提交
653 654 655 656 657 658 659
      {
        match: route('/_next/image'),
        type: 'route',
        name: '_next/image catchall',
        fn: (req, res, _params, parsedUrl) =>
          imageOptimizer(server, req, res, parsedUrl),
      },
T
Tim Neutkens 已提交
660
      {
661
        match: route('/_next/:path*'),
662 663
        type: 'route',
        name: '_next catchall',
T
Tim Neutkens 已提交
664
        // This path is needed because `render()` does a check for `/_next` and the calls the routing again
665
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
666
          await this.render404(req, res, parsedUrl)
667 668 669
          return {
            finished: true,
          }
L
Lukáš Huvar 已提交
670 671
        },
      },
672 673
      ...publicRoutes,
      ...staticFilesRoute,
T
Tim Neutkens 已提交
674
    ]
675

676 677 678 679 680 681
    const getCustomRouteBasePath = (r: { basePath?: false }) => {
      return r.basePath !== false && this.renderOpts.dev
        ? this.nextConfig.basePath
        : ''
    }

682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709
    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 {
710 711
        ...r,
        type,
712
        match,
713 714
        name: type,
        fn: async (_req, _res, _params, _parsedUrl) => ({ finished: false }),
715 716
      } as Route & Rewrite & Header
    }
717 718 719 720 721 722 723 724 725 726 727 728 729 730

    // 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) {
731 732
              key = compileNonPath(key, params)
              value = compileNonPath(value, params)
733
            }
734 735 736 737 738 739 740
            res.setHeader(key, value)
          }
          return { finished: false }
        },
      } as Route
    })

741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756
    // 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
        },
      })
    }

757 758 759 760 761 762
    const redirects = this.customRoutes.redirects.map((redirect) => {
      const redirectRoute = getCustomRoute(redirect, 'redirect')
      return {
        type: redirectRoute.type,
        match: redirectRoute.match,
        statusCode: redirectRoute.statusCode,
763
        name: `Redirect route ${redirectRoute.source}`,
764
        fn: async (req, res, params, parsedUrl) => {
765 766 767
          const { parsedDestination } = prepareDestination(
            redirectRoute.destination,
            params,
768 769 770
            parsedUrl.query,
            false,
            getCustomRouteBasePath(redirectRoute)
771
          )
772 773

          const { query } = parsedDestination
774
          delete (parsedDestination as any).query
775

776
          parsedDestination.search = stringifyQuery(req, query)
777

778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799
          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 {
800
        ...rewriteRoute,
801 802
        check: true,
        type: rewriteRoute.type,
803
        name: `Rewrite route ${rewriteRoute.source}`,
804 805 806 807 808 809
        match: rewriteRoute.match,
        fn: async (req, res, params, parsedUrl) => {
          const { newUrl, parsedDestination } = prepareDestination(
            rewriteRoute.destination,
            params,
            parsedUrl.query,
810 811
            true,
            getCustomRouteBasePath(rewriteRoute)
812
          )
813

814 815
          // external rewrite, proxy it
          if (parsedDestination.protocol) {
816
            const { query } = parsedDestination
817
            delete (parsedDestination as any).query
818
            parsedDestination.search = stringifyQuery(req, query)
819

820 821 822 823 824 825 826 827 828 829 830
            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)
            })
831 832 833
            return {
              finished: true,
            }
834 835
          }
          ;(req as any)._nextRewroteUrl = newUrl
836 837
          ;(req as any)._nextDidRewrite =
            (req as any)._nextRewroteUrl !== req.url
838

839 840 841 842 843 844 845 846
          return {
            finished: false,
            pathname: newUrl,
            query: parsedDestination.query,
          }
        },
      } as Route
    })
847 848 849 850 851 852

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

J
Jan Potoms 已提交
858
        // next.js core assumes page path without trailing slash
859
        pathname = removePathTrailingSlash(pathname)
J
Jan Potoms 已提交
860

861 862 863 864 865 866 867 868 869 870 871 872
        if (this.nextConfig.i18n) {
          const localePathResult = normalizeLocalePath(
            pathname,
            this.nextConfig.i18n?.locales
          )

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

873
        if (params?.path?.[0] === 'api') {
874 875 876
          const handled = await this.handleApiRequest(
            req as NextApiRequest,
            res as NextApiResponse,
877
            pathname,
878
            query
879 880 881 882 883 884 885
          )
          if (handled) {
            return { finished: true }
          }
        }

        await this.render(req, res, pathname, query, parsedUrl)
886 887 888 889
        return {
          finished: true,
        }
      },
890
    }
891

892
    const { useFileSystemPublicRoutes } = this.nextConfig
J
Joe Haddad 已提交
893

894 895
    if (useFileSystemPublicRoutes) {
      this.dynamicRoutes = this.getDynamicRoutes()
896
    }
N
nkzawa 已提交
897

898
    return {
899
      headers,
900
      fsRoutes,
901 902
      rewrites,
      redirects,
903
      catchAllRoute,
904
      useFileSystemPublicRoutes,
905
      dynamicRoutes: this.dynamicRoutes,
906
      basePath: this.nextConfig.basePath,
907
      pageChecker: this.hasPage.bind(this),
908
      locales: this.nextConfig.i18n?.locales,
909
    }
T
Tim Neutkens 已提交
910 911
  }

912
  private async getPagePath(pathname: string): Promise<string> {
913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929
    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
  }

930 931 932 933 934
  protected async _beforeCatchAllRender(
    _req: IncomingMessage,
    _res: ServerResponse,
    _params: Params,
    _parsedUrl: UrlWithParsedQuery
935
  ): Promise<boolean> {
936 937 938
    return false
  }

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

L
Lukáš Huvar 已提交
942 943 944 945 946 947
  /**
   * Resolves `API` request, in development builds on demand
   * @param req http request
   * @param res http response
   * @param pathname path of request
   */
J
Joe Haddad 已提交
948
  private async handleApiRequest(
949 950
    req: IncomingMessage,
    res: ServerResponse,
951 952
    pathname: string,
    query: ParsedUrlQuery
953
  ): Promise<boolean> {
954
    let page = pathname
L
Lukáš Huvar 已提交
955
    let params: Params | boolean = false
956
    let pageFound = await this.hasPage(page)
J
JJ Kasper 已提交
957

958
    if (!pageFound && this.dynamicRoutes) {
L
Lukáš Huvar 已提交
959 960
      for (const dynamicRoute of this.dynamicRoutes) {
        params = dynamicRoute.match(pathname)
961
        if (dynamicRoute.page.startsWith('/api') && params) {
962 963
          page = dynamicRoute.page
          pageFound = true
L
Lukáš Huvar 已提交
964 965 966 967 968
          break
        }
      }
    }

969
    if (!pageFound) {
970
      return false
J
JJ Kasper 已提交
971
    }
972 973 974 975
    // Make sure the page is built before getting the path
    // or else it won't be in the manifest yet
    await this.ensureApiPage(page)

976 977 978 979 980 981 982 983 984 985
    let builtPagePath
    try {
      builtPagePath = await this.getPagePath(page)
    } catch (err) {
      if (err.code === 'ENOENT') {
        return false
      }
      throw err
    }

986
    const pageModule = await require(builtPagePath)
987
    query = { ...query, ...params }
J
JJ Kasper 已提交
988

989
    if (!this.renderOpts.dev && this._isLikeServerless) {
990
      if (typeof pageModule.default === 'function') {
991
        prepareServerlessUrl(req, query)
992 993
        await pageModule.default(req, res)
        return true
J
JJ Kasper 已提交
994 995 996
      }
    }

J
Joe Haddad 已提交
997 998 999 1000 1001
    await apiResolver(
      req,
      res,
      query,
      pageModule,
1002
      this.renderOpts.previewProps,
1003
      false,
J
Joe Haddad 已提交
1004 1005
      this.onErrorMiddleware
    )
1006
    return true
L
Lukáš Huvar 已提交
1007 1008
  }

1009
  protected generatePublicRoutes(): Route[] {
1010
    const publicFiles = new Set(
1011 1012 1013
      recursiveReadDirSync(this.publicDir).map((p) =>
        encodeURI(p.replace(/\\/g, '/'))
      )
1014 1015 1016 1017 1018 1019 1020
    )

    return [
      {
        match: route('/:path*'),
        name: 'public folder catchall',
        fn: async (req, res, params, parsedUrl) => {
1021
          const pathParts: string[] = params.path || []
1022 1023 1024 1025
          const { basePath } = this.nextConfig

          // if basePath is defined require it be present
          if (basePath) {
1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038
            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)
1039 1040
          }

1041
          const path = `/${pathParts.join('/')}`
1042 1043 1044 1045 1046

          if (publicFiles.has(path)) {
            await this.serveStatic(
              req,
              res,
1047
              join(this.publicDir, ...pathParts),
1048 1049
              parsedUrl
            )
1050 1051 1052
            return {
              finished: true,
            }
1053 1054 1055 1056 1057 1058 1059
          }
          return {
            finished: false,
          }
        },
      } as Route,
    ]
1060 1061
  }

1062 1063 1064
  protected getDynamicRoutes(): Array<DynamicRouteItem> {
    const addedPages = new Set<string>()

1065 1066
    return getSortedRoutes(Object.keys(this.pagesManifest!))
      .filter(isDynamicRoute)
1067 1068 1069 1070 1071 1072 1073 1074 1075 1076
      .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 已提交
1077 1078
  }

1079
  private handleCompression(req: IncomingMessage, res: ServerResponse): void {
1080 1081 1082 1083 1084
    if (this.compression) {
      this.compression(req, res, () => {})
    }
  }

1085
  protected async run(
J
Joe Haddad 已提交
1086 1087
    req: IncomingMessage,
    res: ServerResponse,
1088
    parsedUrl: UrlWithParsedQuery
1089
  ): Promise<void> {
1090 1091
    this.handleCompression(req, res)

1092
    try {
1093 1094
      const matched = await this.router.execute(req, res, parsedUrl)
      if (matched) {
1095 1096 1097 1098 1099 1100 1101 1102
        return
      }
    } catch (err) {
      if (err.code === 'DECODE_FAILED') {
        res.statusCode = 400
        return this.renderError(null, req, res, '/_error', {})
      }
      throw err
1103 1104
    }

1105
    await this.render404(req, res, parsedUrl)
N
nkzawa 已提交
1106 1107
  }

1108
  protected async sendHTML(
J
Joe Haddad 已提交
1109 1110
    req: IncomingMessage,
    res: ServerResponse,
1111
    html: string
1112
  ): Promise<void> {
T
Tim Neutkens 已提交
1113
    const { generateEtags, poweredByHeader } = this.renderOpts
1114 1115 1116 1117
    return sendPayload(req, res, html, 'html', {
      generateEtags,
      poweredByHeader,
    })
1118 1119
  }

J
Joe Haddad 已提交
1120 1121 1122 1123 1124
  public async render(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
1125
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1126
  ): Promise<void> {
1127 1128 1129 1130 1131 1132
    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`
      )
    }

1133 1134 1135 1136 1137 1138 1139 1140 1141 1142
    if (
      this.renderOpts.customServer &&
      pathname === '/index' &&
      !(await this.hasPage('/index'))
    ) {
      // maintain backwards compatibility for custom server
      // (see custom-server integration tests)
      pathname = '/'
    }

1143
    const url: any = req.url
1144

1145 1146 1147 1148
    // 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
1149
    if (
1150 1151 1152
      !query._nextDataReq &&
      (url.match(/^\/_next\//) ||
        (this.hasStaticDir && url.match(/^\/static\//)))
1153
    ) {
1154 1155 1156
      return this.handleRequest(req, res, parsedUrl)
    }

1157
    if (isBlockedPage(pathname)) {
1158
      return this.render404(req, res, parsedUrl)
1159 1160
    }

1161
    const html = await this.renderToHTML(req, res, pathname, query)
1162 1163
    // Request was ended by the user
    if (html === null) {
1164 1165 1166
      return
    }

1167
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
1168
  }
N
nkzawa 已提交
1169

J
Joe Haddad 已提交
1170
  private async findPageComponents(
J
Joe Haddad 已提交
1171
    pathname: string,
1172 1173 1174
    query: ParsedUrlQuery = {},
    params: Params | null = null
  ): Promise<FindComponentsResult | null> {
1175
    let paths = [
1176 1177 1178 1179
      // try serving a static AMP version first
      query.amp ? normalizePagePath(pathname) + '.amp' : null,
      pathname,
    ].filter(Boolean)
1180 1181 1182 1183 1184 1185 1186 1187 1188 1189

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

1190
    for (const pagePath of paths) {
J
JJ Kasper 已提交
1191
      try {
1192
        const components = await loadComponents(
J
Joe Haddad 已提交
1193
          this.distDir,
1194 1195
          pagePath!,
          !this.renderOpts.dev && this._isLikeServerless
J
Joe Haddad 已提交
1196
        )
1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208
        // 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
        }

1209 1210 1211
        return {
          components,
          query: {
1212
            ...(components.getStaticProps
1213 1214 1215 1216
              ? {
                  amp: query.amp,
                  _nextDataReq: query._nextDataReq,
                  __nextLocale: query.__nextLocale,
1217
                  __nextDefaultLocale: query.__nextDefaultLocale,
1218
                }
1219 1220 1221 1222
              : query),
            ...(params || {}),
          },
        }
J
JJ Kasper 已提交
1223 1224 1225 1226
      } catch (err) {
        if (err.code !== 'ENOENT') throw err
      }
    }
1227
    return null
J
Joe Haddad 已提交
1228 1229
  }

1230
  protected async getStaticPaths(
1231 1232 1233
    pathname: string
  ): Promise<{
    staticPaths: string[] | undefined
1234
    fallbackMode: 'static' | 'blocking' | false
1235
  }> {
1236 1237 1238 1239 1240
    // `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.
1241 1242
    const fallbackField = this.getPrerenderManifest().dynamicRoutes[pathname]
      .fallback
1243

1244 1245 1246 1247 1248 1249 1250 1251 1252
    return {
      staticPaths,
      fallbackMode:
        typeof fallbackField === 'string'
          ? 'static'
          : fallbackField === null
          ? 'blocking'
          : false,
    }
1253 1254
  }

J
Joe Haddad 已提交
1255 1256 1257 1258
  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1259
    { components, query }: FindComponentsResult,
1260
    opts: RenderOptsPartial
1261
  ): Promise<string | null> {
1262 1263
    const is404Page = pathname === '/404'

1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274
    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

1275
    // we need to ensure the status code if /404 is visited directly
1276
    if (is404Page && !isDataReq) {
1277 1278 1279
      res.statusCode = 404
    }

J
JJ Kasper 已提交
1280
    // handle static page
1281 1282
    if (typeof components.Component === 'string') {
      return components.Component
J
Joe Haddad 已提交
1283 1284
    }

1285 1286 1287 1288
    if (!query.amp) {
      delete query.amp
    }

J
JJ Kasper 已提交
1289
    const locale = query.__nextLocale as string
1290 1291 1292 1293
    const defaultLocale = isSSG
      ? this.nextConfig.i18n?.defaultLocale
      : (query.__nextDefaultLocale as string)

J
Joe Haddad 已提交
1294
    const { i18n } = this.nextConfig
1295
    const locales = i18n.locales as string[]
J
JJ Kasper 已提交
1296

1297 1298 1299 1300 1301 1302 1303 1304
    let previewData: string | false | object | undefined
    let isPreviewMode = false

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

1305 1306 1307
    // 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
1308 1309 1310
    let urlPathname = parseUrl(req.url || '').pathname || '/'

    let resolvedUrlPathname = (req as any)._nextRewroteUrl
1311
      ? (req as any)._nextRewroteUrl
1312
      : urlPathname
1313

1314 1315 1316 1317 1318 1319 1320 1321 1322
    resolvedUrlPathname = removePathTrailingSlash(resolvedUrlPathname)
    urlPathname = removePathTrailingSlash(urlPathname)

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

J
Joe Haddad 已提交
1324
      if (this.nextConfig.i18n) {
J
JJ Kasper 已提交
1325
        return normalizeLocalePath(path, locales).pathname
1326
      }
1327 1328
      return path
    }
1329

1330 1331 1332 1333
    const handleRedirect = (pageData: any) => {
      const redirect = {
        destination: pageData.pageProps.__N_REDIRECT,
        statusCode: pageData.pageProps.__N_REDIRECT_STATUS,
1334
        basePath: pageData.pageProps.__N_REDIRECT_BASE_PATH,
1335 1336
      }
      const statusCode = getRedirectStatus(redirect)
1337 1338 1339 1340 1341
      const { basePath } = this.nextConfig

      if (basePath && redirect.basePath !== false) {
        redirect.destination = `${basePath}${redirect.destination}`
      }
1342 1343 1344 1345 1346 1347 1348 1349 1350 1351

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

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

1352 1353
    // remove /_next/data prefix from urlPathname so it matches
    // for direct page visit and /_next/data visit
1354 1355 1356
    if (isDataReq) {
      resolvedUrlPathname = stripNextDataPath(resolvedUrlPathname)
      urlPathname = stripNextDataPath(urlPathname)
1357 1358
    }

1359
    let ssgCacheKey =
1360 1361
      isPreviewMode || !isSSG
        ? undefined // Preview mode bypasses the cache
1362
        : `${locale ? `/${locale}` : ''}${
1363 1364 1365
            (pathname === '/' || resolvedUrlPathname === '/') && locale
              ? ''
              : resolvedUrlPathname
1366
          }${query.amp ? '.amp' : ''}`
J
JJ Kasper 已提交
1367

1368 1369 1370 1371 1372 1373
    if (is404Page && isSSG) {
      ssgCacheKey = `${locale ? `/${locale}` : ''}${pathname}${
        query.amp ? '.amp' : ''
      }`
    }

J
JJ Kasper 已提交
1374
    // Complete the response with cached data if its present
1375 1376 1377
    const cachedData = ssgCacheKey
      ? await this.incrementalCache.get(ssgCacheKey)
      : undefined
1378

J
JJ Kasper 已提交
1379
    if (cachedData) {
1380
      const data = isDataReq
J
JJ Kasper 已提交
1381 1382 1383
        ? JSON.stringify(cachedData.pageData)
        : cachedData.html

1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394
      const revalidateOptions = !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

1395 1396
      if (!isDataReq && cachedData.pageData?.pageProps?.__N_REDIRECT) {
        await handleRedirect(cachedData.pageData)
1397 1398 1399 1400 1401 1402 1403 1404
      } else if (cachedData.isNotFound) {
        if (revalidateOptions) {
          setRevalidateHeaders(res, revalidateOptions)
        }
        await this.render404(req, res, {
          pathname,
          query,
        } as UrlWithParsedQuery)
1405 1406 1407 1408 1409 1410 1411 1412 1413 1414
      } else {
        sendPayload(
          req,
          res,
          data,
          isDataReq ? 'json' : 'html',
          {
            generateEtags: this.renderOpts.generateEtags,
            poweredByHeader: this.renderOpts.poweredByHeader,
          },
1415
          revalidateOptions
1416 1417
        )
      }
J
JJ Kasper 已提交
1418 1419 1420 1421 1422

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

J
JJ Kasper 已提交
1425
    // If we're here, that means data is missing or it's stale.
1426
    const maybeCoalesceInvoke = ssgCacheKey
1427
      ? (fn: any) => withCoalescedInvoke(fn).bind(null, ssgCacheKey!, [])
1428 1429 1430 1431
      : (fn: any) => async () => {
          const value = await fn()
          return { isOrigin: true, value }
        }
J
JJ Kasper 已提交
1432

1433 1434 1435 1436 1437
    const doRender = maybeCoalesceInvoke(
      async (): Promise<{
        html: string | null
        pageData: any
        sprRevalidate: number | false
1438
        isNotFound?: boolean
1439
        isRedirect?: boolean
1440 1441 1442 1443
      }> => {
        let pageData: any
        let html: string | null
        let sprRevalidate: number | false
1444
        let isNotFound: boolean | undefined
1445
        let isRedirect: boolean | undefined
1446 1447 1448 1449 1450 1451 1452

        let renderResult
        // handle serverless
        if (isLikeServerless) {
          renderResult = await (components.Component as any).renderReqToHTML(
            req,
            res,
P
Prateek Bhatnagar 已提交
1453 1454 1455
            'passthrough',
            {
              fontManifest: this.renderOpts.fontManifest,
1456
              locale,
1457
              locales,
1458
              defaultLocale,
P
Prateek Bhatnagar 已提交
1459
            }
1460
          )
J
JJ Kasper 已提交
1461

1462 1463 1464
          html = renderResult.html
          pageData = renderResult.renderOpts.pageData
          sprRevalidate = renderResult.renderOpts.revalidate
1465
          isNotFound = renderResult.renderOpts.isNotFound
1466
          isRedirect = renderResult.renderOpts.isRedirect
1467
        } else {
1468 1469 1470 1471 1472 1473 1474
          const origQuery = parseUrl(req.url || '', true).query
          const resolvedUrl = formatUrl({
            pathname: resolvedUrlPathname,
            // make sure to only add query values from original URL
            query: origQuery,
          })

1475 1476 1477 1478
          const renderOpts: RenderOpts = {
            ...components,
            ...opts,
            isDataReq,
1479
            resolvedUrl,
1480
            locale,
1481
            locales,
1482
            defaultLocale,
1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493
            // 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,
1494
          }
1495

1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507
          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
1508
          isNotFound = (renderOpts as any).isNotFound
1509
          isRedirect = (renderOpts as any).isRedirect
J
JJ Kasper 已提交
1510 1511
        }

1512
        return { html, pageData, sprRevalidate, isNotFound, isRedirect }
J
JJ Kasper 已提交
1513
      }
1514
    )
J
JJ Kasper 已提交
1515

1516
    const isProduction = !this.renderOpts.dev
J
Joe Haddad 已提交
1517
    const isDynamicPathname = isDynamicRoute(pathname)
1518
    const didRespond = isResSent(res)
1519

1520
    const { staticPaths, fallbackMode } = hasStaticPaths
1521
      ? await this.getStaticPaths(pathname)
1522
      : { staticPaths: undefined, fallbackMode: false }
1523

1524 1525 1526 1527 1528
    // When we did not respond from cache, we need to choose to block on
    // rendering or return a skeleton.
    //
    // * Data requests always block.
    //
1529 1530
    // * Blocking mode fallback always blocks.
    //
1531 1532
    // * Preview mode toggles all pages to be resolved in a blocking manner.
    //
1533
    // * Non-dynamic pages should block (though this is an impossible
1534 1535
    //   case in production).
    //
1536 1537
    // * Dynamic pages should return their skeleton if not defined in
    //   getStaticPaths, then finish the data request on the client-side.
1538
    //
J
Joe Haddad 已提交
1539
    if (
1540
      fallbackMode !== 'blocking' &&
1541
      ssgCacheKey &&
1542 1543 1544
      !didRespond &&
      !isPreviewMode &&
      isDynamicPathname &&
1545 1546
      // Development should trigger fallback when the path is not in
      // `getStaticPaths`
1547 1548
      (isProduction ||
        !staticPaths ||
1549 1550 1551 1552 1553
        // static paths always includes locale so make sure it's prefixed
        // with it
        !staticPaths.includes(
          `${locale ? '/' + locale : ''}${resolvedUrlPathname}`
        ))
J
Joe Haddad 已提交
1554
    ) {
1555 1556 1557 1558 1559
      if (
        // In development, fall through to render to handle missing
        // getStaticPaths.
        (isProduction || staticPaths) &&
        // When fallback isn't present, abort this render so we 404
1560
        fallbackMode !== 'static'
1561
      ) {
1562
        throw new NoFallbackError()
1563 1564
      }

1565 1566
      if (!isDataReq) {
        let html: string
1567

1568 1569
        // Production already emitted the fallback as static HTML.
        if (isProduction) {
1570 1571 1572
          html = await this.incrementalCache.getFallback(
            locale ? `/${locale}${pathname}` : pathname
          )
1573 1574 1575 1576 1577 1578 1579 1580 1581
        }
        // 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
1582 1583
        }

1584 1585 1586 1587 1588 1589
        sendPayload(req, res, html, 'html', {
          generateEtags: this.renderOpts.generateEtags,
          poweredByHeader: this.renderOpts.poweredByHeader,
        })
        return null
      }
1590 1591
    }

1592 1593
    const {
      isOrigin,
1594
      value: { html, pageData, sprRevalidate, isNotFound, isRedirect },
1595
    } = await doRender()
1596
    let resHtml = html
1597

1598 1599 1600 1601 1602 1603 1604 1605 1606
    const revalidateOptions =
      !this.renderOpts.dev || (isServerProps && !isDataReq)
        ? {
            private: isPreviewMode,
            stateful: !isSSG,
            revalidate: sprRevalidate,
          }
        : undefined

1607 1608 1609 1610 1611
    if (
      !isResSent(res) &&
      !isNotFound &&
      (isSSG || isDataReq || isServerProps)
    ) {
1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623
      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,
          },
1624
          revalidateOptions
1625 1626
        )
      }
1627
      resHtml = null
1628
    }
J
JJ Kasper 已提交
1629

1630
    // Update the cache if the head request and cacheable
1631
    if (isOrigin && ssgCacheKey) {
1632 1633
      await this.incrementalCache.set(
        ssgCacheKey,
1634
        { html: html!, pageData, isNotFound, isRedirect },
1635 1636
        sprRevalidate
      )
1637 1638
    }

1639 1640 1641 1642 1643 1644 1645 1646 1647 1648
    if (!isResSent(res) && isNotFound) {
      if (revalidateOptions) {
        setRevalidateHeaders(res, revalidateOptions)
      }
      await this.render404(
        req,
        res,
        { pathname, query } as UrlWithParsedQuery,
        !!revalidateOptions
      )
1649
    }
1650
    return resHtml
1651 1652
  }

1653
  public async renderToHTML(
J
Joe Haddad 已提交
1654 1655 1656
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1657
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1658
  ): Promise<string | null> {
1659 1660 1661
    try {
      const result = await this.findPageComponents(pathname, query)
      if (result) {
1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673
        try {
          return await this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
            result,
            { ...this.renderOpts }
          )
        } catch (err) {
          if (!(err instanceof NoFallbackError)) {
            throw err
          }
1674
        }
1675
      }
J
Joe Haddad 已提交
1676

1677 1678 1679 1680 1681 1682
      if (this.dynamicRoutes) {
        for (const dynamicRoute of this.dynamicRoutes) {
          const params = dynamicRoute.match(pathname)
          if (!params) {
            continue
          }
J
Joe Haddad 已提交
1683

1684
          const dynamicRouteResult = await this.findPageComponents(
1685 1686 1687 1688
            dynamicRoute.page,
            query,
            params
          )
1689
          if (dynamicRouteResult) {
1690 1691 1692 1693 1694
            try {
              return await this.renderToHTMLWithComponents(
                req,
                res,
                dynamicRoute.page,
1695
                dynamicRouteResult,
1696 1697 1698 1699 1700 1701
                { ...this.renderOpts, params }
              )
            } catch (err) {
              if (!(err instanceof NoFallbackError)) {
                throw err
              }
1702
            }
J
Joe Haddad 已提交
1703 1704
          }
        }
1705 1706 1707
      }
    } catch (err) {
      this.logError(err)
1708 1709 1710 1711 1712

      if (err && err.code === 'DECODE_FAILED') {
        res.statusCode = 400
        return await this.renderErrorToHTML(err, req, res, pathname, query)
      }
1713 1714 1715 1716 1717
      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 已提交
1718 1719
  }

J
Joe Haddad 已提交
1720 1721 1722 1723 1724
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1725 1726
    query: ParsedUrlQuery = {},
    setHeaders = true
J
Joe Haddad 已提交
1727
  ): Promise<void> {
1728 1729 1730 1731 1732 1733
    if (setHeaders) {
      res.setHeader(
        'Cache-Control',
        'no-cache, no-store, max-age=0, must-revalidate'
      )
    }
N
Naoyuki Kanezawa 已提交
1734
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
1735
    if (html === null) {
1736 1737
      return
    }
1738
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
1739 1740
  }

1741 1742 1743 1744 1745 1746 1747 1748 1749
  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 已提交
1750 1751 1752 1753 1754
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
1755
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1756
  ) {
1757
    let result: null | FindComponentsResult = null
1758

1759 1760 1761
    const is404 = res.statusCode === 404
    let using404Page = false

1762
    // use static 404 page if available and is 404 response
1763
    if (is404) {
1764
      result = await this.findPageComponents('/404', query)
1765
      using404Page = result !== null
1766 1767 1768 1769 1770 1771
    }

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

1772 1773 1774
    if (
      process.env.NODE_ENV !== 'production' &&
      !using404Page &&
1775 1776
      (await this.hasPage('/_error')) &&
      !(await this.hasPage('/404'))
1777 1778 1779 1780
    ) {
      this.customErrorNo404Warn()
    }

1781
    let html: string | null
1782
    try {
1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793
      try {
        html = await this.renderToHTMLWithComponents(
          req,
          res,
          using404Page ? '/404' : '/_error',
          result!,
          {
            ...this.renderOpts,
            err,
          }
        )
1794 1795
      } catch (maybeFallbackError) {
        if (maybeFallbackError instanceof NoFallbackError) {
1796
          throw new Error('invariant: failed to render error page')
1797
        }
1798
        throw maybeFallbackError
1799
      }
1800 1801
    } catch (renderToHtmlError) {
      console.error(renderToHtmlError)
1802 1803 1804 1805
      res.statusCode = 500
      html = 'Internal Server Error'
    }
    return html
N
Naoyuki Kanezawa 已提交
1806 1807
  }

J
Joe Haddad 已提交
1808 1809 1810
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
1811 1812
    parsedUrl?: UrlWithParsedQuery,
    setHeaders = true
J
Joe Haddad 已提交
1813
  ): Promise<void> {
1814 1815
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
1816 1817 1818 1819 1820 1821 1822
    const { i18n } = this.nextConfig

    if (i18n) {
      query.__nextLocale = query.__nextLocale || i18n.defaultLocale
      query.__nextDefaultLocale =
        query.__nextDefaultLocale || i18n.defaultLocale
    }
N
Naoyuki Kanezawa 已提交
1823
    res.statusCode = 404
1824
    return this.renderError(null, req, res, pathname!, query, setHeaders)
N
Naoyuki Kanezawa 已提交
1825
  }
N
Naoyuki Kanezawa 已提交
1826

J
Joe Haddad 已提交
1827 1828 1829 1830
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
1831
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1832
  ): Promise<void> {
A
Arunoda Susiripala 已提交
1833
    if (!this.isServeableUrl(path)) {
1834
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
1835 1836
    }

1837 1838 1839 1840 1841 1842
    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 已提交
1843
    try {
1844
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
1845
    } catch (err) {
T
Tim Neutkens 已提交
1846
      if (err.code === 'ENOENT' || err.statusCode === 404) {
1847
        this.render404(req, res, parsedUrl)
1848 1849 1850
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
1851 1852 1853 1854 1855 1856
      } else {
        throw err
      }
    }
  }

1857 1858 1859 1860 1861 1862 1863 1864 1865
  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 已提交
1866
      userFilesStatic = recursiveReadDirSync(pathUserFilesStatic).map((f) =>
1867 1868 1869 1870 1871 1872
        join('.', 'static', f)
      )
    }

    let userFilesPublic: string[] = []
    if (this.publicDir && fs.existsSync(this.publicDir)) {
J
Joe Haddad 已提交
1873
      userFilesPublic = recursiveReadDirSync(this.publicDir).map((f) =>
1874 1875 1876 1877 1878 1879 1880
        join('.', 'public', f)
      )
    }

    let nextFilesStatic: string[] = []
    nextFilesStatic = recursiveReadDirSync(
      join(this.distDir, 'static')
J
Joe Haddad 已提交
1881
    ).map((f) => join('.', relative(this.dir, this.distDir), 'static', f))
1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915

    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 已提交
1916
    if (
1917 1918 1919
      (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 已提交
1920 1921 1922 1923
    ) {
      return false
    }

1924 1925 1926 1927
    // Check against the real filesystem paths
    const filesystemUrls = this.getFilesystemPaths()
    const resolved = relative(this.dir, untrustedFilePath)
    return filesystemUrls.has(resolved)
A
Arunoda Susiripala 已提交
1928 1929
  }

1930
  protected readBuildId(): string {
1931 1932 1933 1934 1935
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
1936
        throw new Error(
1937
          `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 已提交
1938
        )
1939 1940 1941
      }

      throw err
1942
    }
1943
  }
1944

1945
  protected get _isLikeServerless(): boolean {
1946 1947
    return isTargetLikeServerless(this.nextConfig.target)
  }
1948
}
1949

1950 1951 1952 1953
function prepareServerlessUrl(
  req: IncomingMessage,
  query: ParsedUrlQuery
): void {
1954 1955 1956 1957 1958 1959 1960 1961 1962 1963
  const curUrl = parseUrl(req.url!, true)
  req.url = formatUrl({
    ...curUrl,
    search: undefined,
    query: {
      ...curUrl.query,
      ...query,
    },
  })
}
1964 1965

class NoFallbackError extends Error {}