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

const getCustomRouteMatcher = pathMatch(true)
87 88 89

type NextConfig = any

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    /**
     * This sets environment variable to be used at the time of SSR by head.tsx.
     * Using this from process.env allows targetting both serverless and SSR by calling
     * `process.env.__NEXT_OPTIMIZE_FONTS`.
     * TODO(prateekbh@): Remove this when experimental.optimizeFonts are being clened up.
     */
    if (this.renderOpts.optimizeFonts) {
      process.env.__NEXT_OPTIMIZE_FONTS = JSON.stringify(true)
    }
266 267 268
    if (this.renderOpts.optimizeImages) {
      process.env.__NEXT_OPTIMIZE_IMAGES = JSON.stringify(true)
    }
N
Naoyuki Kanezawa 已提交
269
  }
N
nkzawa 已提交
270

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

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

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

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

296 297 298
    // Parse the querystring ourselves if the user doesn't handle querystring parsing
    if (typeof parsedUrl.query === 'string') {
      parsedUrl.query = parseQs(parsedUrl.query)
N
Naoyuki Kanezawa 已提交
299
    }
300

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

303 304 305 306 307
    if (basePath && req.url?.startsWith(basePath)) {
      // store original URL to allow checking if basePath was
      // provided or not
      ;(req as any)._nextHadBasePath = true
      req.url = req.url!.replace(basePath, '') || '/'
T
Tim Neutkens 已提交
308 309
    }

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

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

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

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

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

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

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

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

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

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

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

386
      detectedLocale = detectedLocale || i18n.defaultLocale
387

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

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

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

      parsedUrl.query.__nextLocale =
        localePathResult.detectedLocale ||
        detectedDomain?.defaultLocale ||
        defaultLocale
437 438
    }

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

449
  public getRequestHandler() {
450
    return this.handleRequest.bind(this)
N
nkzawa 已提交
451 452
  }

453
  public setAssetPrefix(prefix?: string): void {
454
    this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
455 456
  }

457
  // Backwards compatibility
458
  public async prepare(): Promise<void> {}
N
nkzawa 已提交
459

T
Tim Neutkens 已提交
460
  // Backwards compatibility
461
  protected async close(): Promise<void> {}
T
Tim Neutkens 已提交
462

463
  protected setImmutableAssetCacheControl(res: ServerResponse): void {
T
Tim Neutkens 已提交
464
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
N
nkzawa 已提交
465 466
  }

467
  protected getCustomRoutes(): CustomRoutes {
J
JJ Kasper 已提交
468 469 470
    return require(join(this.distDir, ROUTES_MANIFEST))
  }

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

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

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

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

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

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

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

J
Joe Haddad 已提交
583
          const { i18n } = this.nextConfig
584 585

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

            if (localePathResult.detectedLocale) {
              pathname = localePathResult.pathname
              detectedLocale = localePathResult.detectedLocale
            }
598
            _parsedUrl.query.__nextLocale = detectedLocale!
599 600
          }
          pathname = getRouteFromAssetPath(pathname, '.json')
J
JJ Kasper 已提交
601

J
JJ Kasper 已提交
602
          const parsedUrl = parseUrl(pathname, true)
603

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

639 640 641 642 643 644
    const getCustomRouteBasePath = (r: { basePath?: false }) => {
      return r.basePath !== false && this.renderOpts.dev
        ? this.nextConfig.basePath
        : ''
    }

645 646 647 648
    const getCustomRoute = (r: Rewrite | Redirect | Header, type: RouteType) =>
      ({
        ...r,
        type,
649
        match: getCustomRouteMatcher(`${getCustomRouteBasePath(r)}${r.source}`),
650 651 652 653 654 655 656
        name: type,
        fn: async (_req, _res, _params, _parsedUrl) => ({ finished: false }),
      } as Route & Rewrite & Header)

    const updateHeaderValue = (value: string, params: Params): string => {
      if (!value.includes(':')) {
        return value
657
      }
658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678

      for (const key of Object.keys(params)) {
        if (value.includes(`:${key}`)) {
          value = value
            .replace(
              new RegExp(`:${key}\\*`, 'g'),
              `:${key}--ESCAPED_PARAM_ASTERISKS`
            )
            .replace(
              new RegExp(`:${key}\\?`, 'g'),
              `:${key}--ESCAPED_PARAM_QUESTION`
            )
            .replace(
              new RegExp(`:${key}\\+`, 'g'),
              `:${key}--ESCAPED_PARAM_PLUS`
            )
            .replace(
              new RegExp(`:${key}(?!\\w)`, 'g'),
              `--ESCAPED_PARAM_COLON${key}`
            )
        }
679
      }
680 681 682 683 684 685 686 687 688 689 690 691
      value = value
        .replace(/(:|\*|\?|\+|\(|\)|\{|\})/g, '\\$1')
        .replace(/--ESCAPED_PARAM_PLUS/g, '+')
        .replace(/--ESCAPED_PARAM_COLON/g, ':')
        .replace(/--ESCAPED_PARAM_QUESTION/g, '?')
        .replace(/--ESCAPED_PARAM_ASTERISKS/g, '*')

      // the value needs to start with a forward-slash to be compiled
      // correctly
      return compilePathToRegex(`/${value}`, { validate: false })(
        params
      ).substr(1)
692
    }
693

694 695 696 697 698 699 700 701 702 703 704 705 706 707 708
    // Headers come very first
    const headers = this.customRoutes.headers.map((r) => {
      const headerRoute = getCustomRoute(r, 'header')
      return {
        match: headerRoute.match,
        type: headerRoute.type,
        name: `${headerRoute.type} ${headerRoute.source} header route`,
        fn: async (_req, res, params, _parsedUrl) => {
          const hasParams = Object.keys(params).length > 0

          for (const header of (headerRoute as Header).headers) {
            let { key, value } = header
            if (hasParams) {
              key = updateHeaderValue(key, params)
              value = updateHeaderValue(value, params)
709
            }
710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727
            res.setHeader(key, value)
          }
          return { finished: false }
        },
      } as Route
    })

    const redirects = this.customRoutes.redirects.map((redirect) => {
      const redirectRoute = getCustomRoute(redirect, 'redirect')
      return {
        type: redirectRoute.type,
        match: redirectRoute.match,
        statusCode: redirectRoute.statusCode,
        name: `Redirect route`,
        fn: async (_req, res, params, parsedUrl) => {
          const { parsedDestination } = prepareDestination(
            redirectRoute.destination,
            params,
728 729 730
            parsedUrl.query,
            false,
            getCustomRouteBasePath(redirectRoute)
731
          )
732 733

          const { query } = parsedDestination
734
          delete (parsedDestination as any).query
735

736
          parsedDestination.search = stringifyQs(query)
737

738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759
          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 {
760
        ...rewriteRoute,
761 762 763 764 765 766 767 768 769
        check: true,
        type: rewriteRoute.type,
        name: `Rewrite route`,
        match: rewriteRoute.match,
        fn: async (req, res, params, parsedUrl) => {
          const { newUrl, parsedDestination } = prepareDestination(
            rewriteRoute.destination,
            params,
            parsedUrl.query,
770 771
            true,
            getCustomRouteBasePath(rewriteRoute)
772
          )
773

774 775
          // external rewrite, proxy it
          if (parsedDestination.protocol) {
776
            const { query } = parsedDestination
777
            delete (parsedDestination as any).query
778
            parsedDestination.search = stringifyQs(query)
779

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

799 800 801 802 803 804 805 806
          return {
            finished: false,
            pathname: newUrl,
            query: parsedDestination.query,
          }
        },
      } as Route
    })
807 808 809 810 811 812

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

J
Jan Potoms 已提交
818
        // next.js core assumes page path without trailing slash
819
        pathname = removePathTrailingSlash(pathname)
J
Jan Potoms 已提交
820

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

        await this.render(req, res, pathname, query, parsedUrl)
834 835 836 837
        return {
          finished: true,
        }
      },
838
    }
839

840
    const { useFileSystemPublicRoutes } = this.nextConfig
J
Joe Haddad 已提交
841

842 843
    if (useFileSystemPublicRoutes) {
      this.dynamicRoutes = this.getDynamicRoutes()
844
    }
N
nkzawa 已提交
845

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

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

877 878 879 880 881
  protected async _beforeCatchAllRender(
    _req: IncomingMessage,
    _res: ServerResponse,
    _params: Params,
    _parsedUrl: UrlWithParsedQuery
882
  ): Promise<boolean> {
883 884 885
    return false
  }

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

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

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

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

923 924 925 926 927 928 929 930 931 932
    let builtPagePath
    try {
      builtPagePath = await this.getPagePath(page)
    } catch (err) {
      if (err.code === 'ENOENT') {
        return false
      }
      throw err
    }

933
    const pageModule = await require(builtPagePath)
934
    query = { ...query, ...params }
J
JJ Kasper 已提交
935

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

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

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

    return [
      {
        match: route('/:path*'),
        name: 'public folder catchall',
        fn: async (req, res, params, parsedUrl) => {
968
          const pathParts: string[] = params.path || []
969 970 971 972 973 974 975 976
          const { basePath } = this.nextConfig

          // if basePath is defined require it be present
          if (basePath) {
            if (pathParts[0] !== basePath.substr(1)) return { finished: false }
            pathParts.shift()
          }

977
          const path = `/${pathParts.join('/')}`
978 979 980 981 982

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

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

1007
  private handleCompression(req: IncomingMessage, res: ServerResponse): void {
1008 1009 1010 1011 1012
    if (this.compression) {
      this.compression(req, res, () => {})
    }
  }

1013
  protected async run(
J
Joe Haddad 已提交
1014 1015
    req: IncomingMessage,
    res: ServerResponse,
1016
    parsedUrl: UrlWithParsedQuery
1017
  ): Promise<void> {
1018 1019
    this.handleCompression(req, res)

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

1033
    await this.render404(req, res, parsedUrl)
N
nkzawa 已提交
1034 1035
  }

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

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

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

1071
    const url: any = req.url
1072

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

1085
    if (isBlockedPage(pathname)) {
1086
      return this.render404(req, res, parsedUrl)
1087 1088
    }

1089
    const html = await this.renderToHTML(req, res, pathname, query)
1090 1091
    // Request was ended by the user
    if (html === null) {
1092 1093 1094
      return
    }

1095
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
1096
  }
N
nkzawa 已提交
1097

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

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

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

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

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

1171 1172 1173 1174 1175 1176 1177 1178 1179
    return {
      staticPaths,
      fallbackMode:
        typeof fallbackField === 'string'
          ? 'static'
          : fallbackField === null
          ? 'blocking'
          : false,
    }
1180 1181
  }

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

1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201
    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

1202
    // we need to ensure the status code if /404 is visited directly
1203
    if (is404Page && !isDataReq) {
1204 1205 1206
      res.statusCode = 404
    }

J
JJ Kasper 已提交
1207
    // handle static page
1208 1209
    if (typeof components.Component === 'string') {
      return components.Component
J
Joe Haddad 已提交
1210 1211
    }

1212 1213 1214 1215
    if (!query.amp) {
      delete query.amp
    }

J
JJ Kasper 已提交
1216 1217
    const locale = query.__nextLocale as string
    delete query.__nextLocale
1218

J
Joe Haddad 已提交
1219
    const { i18n } = this.nextConfig
1220
    const locales = i18n.locales as string[]
J
JJ Kasper 已提交
1221

1222 1223 1224 1225 1226 1227 1228 1229
    let previewData: string | false | object | undefined
    let isPreviewMode = false

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

1230 1231 1232
    // 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
1233 1234 1235
    let urlPathname = parseUrl(req.url || '').pathname || '/'

    let resolvedUrlPathname = (req as any)._nextRewroteUrl
1236
      ? (req as any)._nextRewroteUrl
1237
      : urlPathname
1238

1239 1240 1241 1242 1243 1244 1245 1246 1247
    resolvedUrlPathname = removePathTrailingSlash(resolvedUrlPathname)
    urlPathname = removePathTrailingSlash(urlPathname)

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

J
Joe Haddad 已提交
1249
      if (this.nextConfig.i18n) {
J
JJ Kasper 已提交
1250
        return normalizeLocalePath(path, locales).pathname
1251
      }
1252 1253
      return path
    }
1254

1255 1256
    // remove /_next/data prefix from urlPathname so it matches
    // for direct page visit and /_next/data visit
1257 1258 1259
    if (isDataReq) {
      resolvedUrlPathname = stripNextDataPath(resolvedUrlPathname)
      urlPathname = stripNextDataPath(urlPathname)
1260 1261
    }

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

1269 1270 1271 1272 1273 1274
    if (is404Page && isSSG) {
      ssgCacheKey = `${locale ? `/${locale}` : ''}${pathname}${
        query.amp ? '.amp' : ''
      }`
    }

J
JJ Kasper 已提交
1275
    // Complete the response with cached data if its present
1276 1277 1278
    const cachedData = ssgCacheKey
      ? await this.incrementalCache.get(ssgCacheKey)
      : undefined
1279

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

1287
      const data = isDataReq
J
JJ Kasper 已提交
1288 1289 1290
        ? JSON.stringify(cachedData.pageData)
        : cachedData.html

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

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

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

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

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

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

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

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

1401
        return { html, pageData, sprRevalidate, isNotFound }
J
JJ Kasper 已提交
1402
      }
1403
    )
J
JJ Kasper 已提交
1404

1405
    const isProduction = !this.renderOpts.dev
J
Joe Haddad 已提交
1406
    const isDynamicPathname = isDynamicRoute(pathname)
1407
    const didRespond = isResSent(res)
1408

1409
    const { staticPaths, fallbackMode } = hasStaticPaths
1410
      ? await this.getStaticPaths(pathname)
1411
      : { staticPaths: undefined, fallbackMode: false }
1412

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

1454 1455
      if (!isDataReq) {
        let html: string
1456

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

1473 1474 1475 1476 1477 1478
        sendPayload(req, res, html, 'html', {
          generateEtags: this.renderOpts.generateEtags,
          poweredByHeader: this.renderOpts.poweredByHeader,
        })
        return null
      }
1479 1480
    }

1481 1482
    const {
      isOrigin,
1483
      value: { html, pageData, sprRevalidate, isNotFound },
1484
    } = await doRender()
1485
    let resHtml = html
1486 1487 1488 1489 1490 1491

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

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

1521 1522 1523
    if (isNotFound) {
      throw new NoFallbackError()
    }
1524
    return resHtml
1525 1526
  }

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

1551 1552 1553 1554 1555 1556
      if (this.dynamicRoutes) {
        for (const dynamicRoute of this.dynamicRoutes) {
          const params = dynamicRoute.match(pathname)
          if (!params) {
            continue
          }
J
Joe Haddad 已提交
1557

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

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

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

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

1630 1631 1632
    const is404 = res.statusCode === 404
    let using404Page = false

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

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

1643 1644 1645
    if (
      process.env.NODE_ENV !== 'production' &&
      !using404Page &&
1646 1647
      (await this.hasPage('/_error')) &&
      !(await this.hasPage('/404'))
1648 1649 1650 1651
    ) {
      this.customErrorNo404Warn()
    }

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

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

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

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

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

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

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

    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 已提交
1779
    if (
1780 1781 1782
      (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 已提交
1783 1784 1785 1786
    ) {
      return false
    }

1787 1788 1789 1790
    // Check against the real filesystem paths
    const filesystemUrls = this.getFilesystemPaths()
    const resolved = relative(this.dir, untrustedFilePath)
    return filesystemUrls.has(resolved)
A
Arunoda Susiripala 已提交
1791 1792
  }

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

      throw err
1805
    }
1806
  }
1807

1808
  protected get _isLikeServerless(): boolean {
1809 1810
    return isTargetLikeServerless(this.nextConfig.target)
  }
1811
}
1812

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

class NoFallbackError extends Error {}