next-server.ts 57.3 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
        parsedUrl.pathname = `${basePath || ''}${localePathResult.pathname}`
361 362 363 364 365
      }

      // 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
366
      if (detectedDomain && pathname === '/') {
367 368 369 370
        const localeToCheck = acceptPreferredLocale
        // const localeToCheck = localePathResult.detectedLocale
        //   ? detectedLocale
        //   : acceptPreferredLocale
371

372 373 374
        const matchedDomain = detectDomainLocale(
          i18n.domains,
          undefined,
375
          localeToCheck
376 377
        )

378 379 380 381 382
        if (
          matchedDomain &&
          (matchedDomain.domain !== detectedDomain.domain ||
            localeToCheck !== matchedDomain.defaultLocale)
        ) {
383 384
          localeDomainRedirect = `http${matchedDomain.http ? '' : 's'}://${
            matchedDomain.domain
385 386
          }/${
            localeToCheck === matchedDomain.defaultLocale ? '' : localeToCheck
387
          }`
388 389 390
        }
      }

391
      const denormalizedPagePath = denormalizePagePath(pathname || '/')
392
      const detectedDefaultLocale =
393 394
        !detectedLocale ||
        detectedLocale.toLowerCase() === defaultLocale.toLowerCase()
395 396 397 398
      const shouldStripDefaultLocale = false
      // detectedDefaultLocale &&
      // denormalizedPagePath.toLowerCase() ===
      //   `/${i18n.defaultLocale.toLowerCase()}`
399

400 401
      const shouldAddLocalePrefix =
        !detectedDefaultLocale && denormalizedPagePath === '/'
402

403
      detectedLocale = detectedLocale || i18n.defaultLocale
404

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

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

450 451 452
      parsedUrl.query.__nextDefaultLocale =
        detectedDomain?.defaultLocale || i18n.defaultLocale

453 454 455 456
      parsedUrl.query.__nextLocale =
        localePathResult.detectedLocale ||
        detectedDomain?.defaultLocale ||
        defaultLocale
457 458
    }

459
    res.statusCode = 200
460 461 462
    try {
      return await this.run(req, res, parsedUrl)
    } catch (err) {
J
Joe Haddad 已提交
463 464 465
      this.logError(err)
      res.statusCode = 500
      res.end('Internal Server Error')
466
    }
467 468
  }

469
  public getRequestHandler() {
470
    return this.handleRequest.bind(this)
N
nkzawa 已提交
471 472
  }

473
  public setAssetPrefix(prefix?: string): void {
474
    this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
475 476
  }

477
  // Backwards compatibility
478
  public async prepare(): Promise<void> {}
N
nkzawa 已提交
479

T
Tim Neutkens 已提交
480
  // Backwards compatibility
481
  protected async close(): Promise<void> {}
T
Tim Neutkens 已提交
482

483
  protected setImmutableAssetCacheControl(res: ServerResponse): void {
T
Tim Neutkens 已提交
484
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
N
nkzawa 已提交
485 486
  }

487
  protected getCustomRoutes(): CustomRoutes {
J
JJ Kasper 已提交
488 489 490
    return require(join(this.distDir, ROUTES_MANIFEST))
  }

491 492 493 494
  private _cachedPreviewManifest: PrerenderManifest | undefined
  protected getPrerenderManifest(): PrerenderManifest {
    if (this._cachedPreviewManifest) {
      return this._cachedPreviewManifest
J
Joe Haddad 已提交
495
    }
496 497 498 499 500 501
    const manifest = require(join(this.distDir, PRERENDER_MANIFEST))
    return (this._cachedPreviewManifest = manifest)
  }

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

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

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

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

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

          // re-create page's pathname
602
          let pathname = `/${params.path.join('/')}`
603
          pathname = getRouteFromAssetPath(pathname, '.json')
604

J
Joe Haddad 已提交
605
          const { i18n } = this.nextConfig
606 607

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

            let detectedLocale = ''
616 617 618 619 620

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

622
            _parsedUrl.query.__nextLocale = detectedLocale!
623 624
            _parsedUrl.query.__nextDefaultLocale =
              defaultLocale || i18n.defaultLocale
625 626 627 628 629 630 631

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

J
JJ Kasper 已提交
634
          const parsedUrl = parseUrl(pathname, true)
635

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

671 672 673 674 675 676
    const getCustomRouteBasePath = (r: { basePath?: false }) => {
      return r.basePath !== false && this.renderOpts.dev
        ? this.nextConfig.basePath
        : ''
    }

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

    // 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) {
726 727
              key = compileNonPath(key, params)
              value = compileNonPath(value, params)
728
            }
729 730 731 732 733 734 735
            res.setHeader(key, value)
          }
          return { finished: false }
        },
      } as Route
    })

736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751
    // 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
        },
      })
    }

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

          const { query } = parsedDestination
769
          delete (parsedDestination as any).query
770

771
          parsedDestination.search = stringifyQuery(req, query)
772

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

809 810
          // external rewrite, proxy it
          if (parsedDestination.protocol) {
811
            const { query } = parsedDestination
812
            delete (parsedDestination as any).query
813
            parsedDestination.search = stringifyQuery(req, query)
814

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

834 835 836 837 838 839 840 841
          return {
            finished: false,
            pathname: newUrl,
            query: parsedDestination.query,
          }
        },
      } as Route
    })
842 843 844 845 846 847

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

J
Jan Potoms 已提交
853
        // next.js core assumes page path without trailing slash
854
        pathname = removePathTrailingSlash(pathname)
J
Jan Potoms 已提交
855

856 857 858 859 860 861 862 863 864 865 866 867
        if (this.nextConfig.i18n) {
          const localePathResult = normalizeLocalePath(
            pathname,
            this.nextConfig.i18n?.locales
          )

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

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

        await this.render(req, res, pathname, query, parsedUrl)
881 882 883 884
        return {
          finished: true,
        }
      },
885
    }
886

887
    const { useFileSystemPublicRoutes } = this.nextConfig
J
Joe Haddad 已提交
888

889 890
    if (useFileSystemPublicRoutes) {
      this.dynamicRoutes = this.getDynamicRoutes()
891
    }
N
nkzawa 已提交
892

893
    return {
894
      headers,
895
      fsRoutes,
896 897
      rewrites,
      redirects,
898
      catchAllRoute,
899
      useFileSystemPublicRoutes,
900
      dynamicRoutes: this.dynamicRoutes,
901
      basePath: this.nextConfig.basePath,
902
      pageChecker: this.hasPage.bind(this),
903
      locales: this.nextConfig.i18n?.locales,
904
    }
T
Tim Neutkens 已提交
905 906
  }

907
  private async getPagePath(pathname: string): Promise<string> {
908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924
    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
  }

925 926 927 928 929
  protected async _beforeCatchAllRender(
    _req: IncomingMessage,
    _res: ServerResponse,
    _params: Params,
    _parsedUrl: UrlWithParsedQuery
930
  ): Promise<boolean> {
931 932 933
    return false
  }

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

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

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

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

971 972 973 974 975 976 977 978 979 980
    let builtPagePath
    try {
      builtPagePath = await this.getPagePath(page)
    } catch (err) {
      if (err.code === 'ENOENT') {
        return false
      }
      throw err
    }

981
    const pageModule = await require(builtPagePath)
982
    query = { ...query, ...params }
J
JJ Kasper 已提交
983

984
    if (!this.renderOpts.dev && this._isLikeServerless) {
985
      if (typeof pageModule.default === 'function') {
986
        prepareServerlessUrl(req, query)
987 988
        await pageModule.default(req, res)
        return true
J
JJ Kasper 已提交
989 990 991
      }
    }

J
Joe Haddad 已提交
992 993 994 995 996
    await apiResolver(
      req,
      res,
      query,
      pageModule,
997
      this.renderOpts.previewProps,
998
      false,
J
Joe Haddad 已提交
999 1000
      this.onErrorMiddleware
    )
1001
    return true
L
Lukáš Huvar 已提交
1002 1003
  }

1004
  protected generatePublicRoutes(): Route[] {
1005
    const publicFiles = new Set(
1006 1007 1008
      recursiveReadDirSync(this.publicDir).map((p) =>
        encodeURI(p.replace(/\\/g, '/'))
      )
1009 1010 1011 1012 1013 1014 1015
    )

    return [
      {
        match: route('/:path*'),
        name: 'public folder catchall',
        fn: async (req, res, params, parsedUrl) => {
1016
          const pathParts: string[] = params.path || []
1017 1018 1019 1020
          const { basePath } = this.nextConfig

          // if basePath is defined require it be present
          if (basePath) {
1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033
            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)
1034 1035
          }

1036
          const path = `/${pathParts.join('/')}`
1037 1038 1039 1040 1041

          if (publicFiles.has(path)) {
            await this.serveStatic(
              req,
              res,
1042
              join(this.publicDir, ...pathParts),
1043 1044
              parsedUrl
            )
1045 1046 1047
            return {
              finished: true,
            }
1048 1049 1050 1051 1052 1053 1054
          }
          return {
            finished: false,
          }
        },
      } as Route,
    ]
1055 1056
  }

1057 1058 1059
  protected getDynamicRoutes(): Array<DynamicRouteItem> {
    const addedPages = new Set<string>()

1060 1061
    return getSortedRoutes(Object.keys(this.pagesManifest!))
      .filter(isDynamicRoute)
1062 1063 1064 1065 1066 1067 1068 1069 1070 1071
      .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 已提交
1072 1073
  }

1074
  private handleCompression(req: IncomingMessage, res: ServerResponse): void {
1075 1076 1077 1078 1079
    if (this.compression) {
      this.compression(req, res, () => {})
    }
  }

1080
  protected async run(
J
Joe Haddad 已提交
1081 1082
    req: IncomingMessage,
    res: ServerResponse,
1083
    parsedUrl: UrlWithParsedQuery
1084
  ): Promise<void> {
1085 1086
    this.handleCompression(req, res)

1087
    try {
1088 1089
      const matched = await this.router.execute(req, res, parsedUrl)
      if (matched) {
1090 1091 1092 1093 1094 1095 1096 1097
        return
      }
    } catch (err) {
      if (err.code === 'DECODE_FAILED') {
        res.statusCode = 400
        return this.renderError(null, req, res, '/_error', {})
      }
      throw err
1098 1099
    }

1100
    await this.render404(req, res, parsedUrl)
N
nkzawa 已提交
1101 1102
  }

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

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

1128 1129 1130 1131 1132 1133 1134 1135 1136 1137
    if (
      this.renderOpts.customServer &&
      pathname === '/index' &&
      !(await this.hasPage('/index'))
    ) {
      // maintain backwards compatibility for custom server
      // (see custom-server integration tests)
      pathname = '/'
    }

1138
    const url: any = req.url
1139

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

1152
    if (isBlockedPage(pathname)) {
1153
      return this.render404(req, res, parsedUrl)
1154 1155
    }

1156
    const html = await this.renderToHTML(req, res, pathname, query)
1157 1158
    // Request was ended by the user
    if (html === null) {
1159 1160 1161
      return
    }

1162
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
1163
  }
N
nkzawa 已提交
1164

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

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

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

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

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

1239 1240 1241 1242 1243 1244 1245 1246 1247
    return {
      staticPaths,
      fallbackMode:
        typeof fallbackField === 'string'
          ? 'static'
          : fallbackField === null
          ? 'blocking'
          : false,
    }
1248 1249
  }

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

1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269
    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

1270
    // we need to ensure the status code if /404 is visited directly
1271
    if (is404Page && !isDataReq) {
1272 1273 1274
      res.statusCode = 404
    }

J
JJ Kasper 已提交
1275
    // handle static page
1276 1277
    if (typeof components.Component === 'string') {
      return components.Component
J
Joe Haddad 已提交
1278 1279
    }

1280 1281 1282 1283
    if (!query.amp) {
      delete query.amp
    }

J
JJ Kasper 已提交
1284
    const locale = query.__nextLocale as string
1285 1286 1287 1288
    const defaultLocale = isSSG
      ? this.nextConfig.i18n?.defaultLocale
      : (query.__nextDefaultLocale as string)

J
Joe Haddad 已提交
1289
    const { i18n } = this.nextConfig
1290
    const locales = i18n.locales as string[]
J
JJ Kasper 已提交
1291

1292 1293 1294 1295 1296 1297 1298 1299
    let previewData: string | false | object | undefined
    let isPreviewMode = false

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

1300 1301 1302
    // 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
1303 1304 1305
    let urlPathname = parseUrl(req.url || '').pathname || '/'

    let resolvedUrlPathname = (req as any)._nextRewroteUrl
1306
      ? (req as any)._nextRewroteUrl
1307
      : urlPathname
1308

1309 1310 1311 1312 1313 1314 1315 1316 1317
    resolvedUrlPathname = removePathTrailingSlash(resolvedUrlPathname)
    urlPathname = removePathTrailingSlash(urlPathname)

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

J
Joe Haddad 已提交
1319
      if (this.nextConfig.i18n) {
J
JJ Kasper 已提交
1320
        return normalizeLocalePath(path, locales).pathname
1321
      }
1322 1323
      return path
    }
1324

1325 1326 1327 1328
    const handleRedirect = (pageData: any) => {
      const redirect = {
        destination: pageData.pageProps.__N_REDIRECT,
        statusCode: pageData.pageProps.__N_REDIRECT_STATUS,
1329
        basePath: pageData.pageProps.__N_REDIRECT_BASE_PATH,
1330 1331
      }
      const statusCode = getRedirectStatus(redirect)
1332 1333 1334 1335 1336
      const { basePath } = this.nextConfig

      if (basePath && redirect.basePath !== false) {
        redirect.destination = `${basePath}${redirect.destination}`
      }
1337 1338 1339 1340 1341 1342 1343 1344 1345 1346

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

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

1347 1348
    // remove /_next/data prefix from urlPathname so it matches
    // for direct page visit and /_next/data visit
1349 1350 1351
    if (isDataReq) {
      resolvedUrlPathname = stripNextDataPath(resolvedUrlPathname)
      urlPathname = stripNextDataPath(urlPathname)
1352 1353
    }

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

1363 1364 1365 1366 1367 1368
    if (is404Page && isSSG) {
      ssgCacheKey = `${locale ? `/${locale}` : ''}${pathname}${
        query.amp ? '.amp' : ''
      }`
    }

J
JJ Kasper 已提交
1369
    // Complete the response with cached data if its present
1370 1371 1372
    const cachedData = ssgCacheKey
      ? await this.incrementalCache.get(ssgCacheKey)
      : undefined
1373

J
JJ Kasper 已提交
1374
    if (cachedData) {
1375
      const data = isDataReq
J
JJ Kasper 已提交
1376 1377 1378
        ? JSON.stringify(cachedData.pageData)
        : cachedData.html

1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389
      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

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

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

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

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

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

1457 1458 1459
          html = renderResult.html
          pageData = renderResult.renderOpts.pageData
          sprRevalidate = renderResult.renderOpts.revalidate
1460
          isNotFound = renderResult.renderOpts.isNotFound
1461
          isRedirect = renderResult.renderOpts.isRedirect
1462
        } else {
1463
          const origQuery = parseUrl(req.url || '', true).query
1464 1465 1466
          const hadTrailingSlash =
            urlPathname !== '/' && this.nextConfig.trailingSlash

1467
          const resolvedUrl = formatUrl({
1468
            pathname: `${resolvedUrlPathname}${hadTrailingSlash ? '/' : ''}`,
1469 1470 1471 1472
            // make sure to only add query values from original URL
            query: origQuery,
          })

1473 1474 1475 1476
          const renderOpts: RenderOpts = {
            ...components,
            ...opts,
            isDataReq,
1477
            resolvedUrl,
1478
            locale,
1479
            locales,
1480
            defaultLocale,
1481 1482 1483 1484 1485 1486 1487
            // 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
1488
                  pathname: `${urlPathname}${hadTrailingSlash ? '/' : ''}`,
1489 1490 1491
                  query: origQuery,
                })
              : resolvedUrl,
1492
          }
1493

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

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

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

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

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

1563 1564
      if (!isDataReq) {
        let html: string
1565

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

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

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

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

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

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

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

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

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

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

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

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

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

1757 1758 1759
    const is404 = res.statusCode === 404
    let using404Page = false

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

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

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

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

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

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

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

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

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

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

    let nextFilesStatic: string[] = []
    nextFilesStatic = recursiveReadDirSync(
      join(this.distDir, 'static')
J
Joe Haddad 已提交
1879
    ).map((f) => join('.', relative(this.dir, this.distDir), 'static', f))
1880 1881 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

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

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

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

      throw err
1940
    }
1941
  }
1942

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

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

class NoFallbackError extends Error {}