next-server.ts 41.9 KB
Newer Older
G
Guy Bedford 已提交
1
import compression from 'next/dist/compiled/compression'
J
Joe Haddad 已提交
2
import fs from 'fs'
3
import chalk from 'next/dist/compiled/chalk'
J
Joe Haddad 已提交
4
import { IncomingMessage, ServerResponse } from 'http'
G
Guy Bedford 已提交
5
import Proxy from 'next/dist/compiled/http-proxy'
6
import { join, relative, resolve, sep } from 'path'
7
import { parse as parseQs, ParsedUrlQuery } from 'querystring'
8
import { format as formatUrl, parse as parseUrl, UrlWithParsedQuery } from 'url'
9
import { PrerenderManifest } from '../../build'
J
Joe Haddad 已提交
10 11 12 13 14 15 16
import {
  getRedirectStatus,
  Header,
  Redirect,
  Rewrite,
  RouteType,
} from '../../lib/check-custom-routes'
J
JJ Kasper 已提交
17
import { withCoalescedInvoke } from '../../lib/coalesced-function'
J
Joe Haddad 已提交
18 19
import {
  BUILD_ID_FILE,
20
  CLIENT_PUBLIC_FILES_PATH,
J
Joe Haddad 已提交
21 22
  CLIENT_STATIC_FILES_PATH,
  CLIENT_STATIC_FILES_RUNTIME,
23
  PAGES_MANIFEST,
J
Joe Haddad 已提交
24
  PHASE_PRODUCTION_SERVER,
J
Joe Haddad 已提交
25
  PRERENDER_MANIFEST,
26
  ROUTES_MANIFEST,
27
  SERVERLESS_DIRECTORY,
J
Joe Haddad 已提交
28
  SERVER_DIRECTORY,
T
Tim Neutkens 已提交
29
} from '../lib/constants'
J
Joe Haddad 已提交
30 31 32 33
import {
  getRouteMatcher,
  getRouteRegex,
  getSortedRoutes,
34
  isDynamicRoute,
J
Joe Haddad 已提交
35
} from '../lib/router/utils'
36
import * as envConfig from '../lib/runtime-config'
J
Joe Haddad 已提交
37
import { isResSent, NextApiRequest, NextApiResponse } from '../lib/utils'
J
Joe Haddad 已提交
38
import { apiResolver, tryGetPreviewData, __ApiPreviewProps } from './api-utils'
39
import loadConfig, { isTargetLikeServerless } from './config'
40
import pathMatch from './lib/path-match'
J
Joe Haddad 已提交
41
import { recursiveReadDirSync } from './lib/recursive-readdir-sync'
42
import { loadComponents, LoadComponentsReturnType } from './load-components'
J
Joe Haddad 已提交
43
import { normalizePagePath } from './normalize-page-path'
44
import { RenderOpts, RenderOptsPartial, renderToHTML } from './render'
J
Joe Haddad 已提交
45
import { getPagePath } from './require'
46 47 48
import Router, {
  DynamicRoutes,
  PageChecker,
J
Joe Haddad 已提交
49
  Params,
50
  prepareDestination,
J
Joe Haddad 已提交
51 52
  route,
  Route,
53
} from './router'
J
Joe Haddad 已提交
54
import { sendHTML } from './send-html'
55
import { sendPayload } from './send-payload'
J
Joe Haddad 已提交
56
import { serveStatic } from './serve-static'
57
import {
J
Joe Haddad 已提交
58
  getFallback,
59 60 61 62
  getSprCache,
  initializeSprCache,
  setSprCache,
} from './spr-cache'
63
import { execOnce } from '../lib/utils'
64
import { isBlockedPage } from './utils'
65
import { compile as compilePathToRegex } from 'next/dist/compiled/path-to-regexp'
66
import { loadEnvConfig } from '../../lib/load-env-config'
67
import './node-polyfill-fetch'
J
Jan Potoms 已提交
68
import { PagesManifest } from '../../build/webpack/plugins/pages-manifest-plugin'
J
JJ Kasper 已提交
69 70

const getCustomRouteMatcher = pathMatch(true)
71 72 73

type NextConfig = any

74 75 76 77 78 79
type Middleware = (
  req: IncomingMessage,
  res: ServerResponse,
  next: (err?: Error) => void
) => void

80 81 82 83 84
type FindComponentsResult = {
  components: LoadComponentsReturnType
  query: ParsedUrlQuery
}

T
Tim Neutkens 已提交
85
export type ServerConstructor = {
86 87 88
  /**
   * Where the Next project is located - @default '.'
   */
J
Joe Haddad 已提交
89
  dir?: string
90 91 92
  /**
   * Hide error messages containing server information - @default false
   */
J
Joe Haddad 已提交
93
  quiet?: boolean
94 95 96
  /**
   * Object what you would use in next.config.js - @default {}
   */
97
  conf?: NextConfig
J
JJ Kasper 已提交
98
  dev?: boolean
99
  customServer?: boolean
100
}
101

N
nkzawa 已提交
102
export default class Server {
103 104 105 106
  dir: string
  quiet: boolean
  nextConfig: NextConfig
  distDir: string
107
  pagesDir?: string
108
  publicDir: string
109
  hasStaticDir: boolean
110
  serverBuildDir: string
J
Jan Potoms 已提交
111
  pagesManifest?: PagesManifest
112 113
  buildId: string
  renderOpts: {
T
Tim Neutkens 已提交
114
    poweredByHeader: boolean
J
Joe Haddad 已提交
115 116 117
    buildId: string
    generateEtags: boolean
    runtimeConfig?: { [key: string]: any }
118 119 120
    assetPrefix?: string
    canonicalBase: string
    dev?: boolean
121
    previewProps: __ApiPreviewProps
122
    customServer?: boolean
123
    ampOptimizerConfig?: { [key: string]: any }
124
    basePath: string
125
  }
126
  private compression?: Middleware
J
JJ Kasper 已提交
127
  private onErrorMiddleware?: ({ err }: { err: Error }) => Promise<void>
128
  router: Router
129
  protected dynamicRoutes?: DynamicRoutes
J
JJ Kasper 已提交
130 131 132
  protected customRoutes?: {
    rewrites: Rewrite[]
    redirects: Redirect[]
133
    headers: Header[]
J
JJ Kasper 已提交
134
  }
135 136 137
  protected staticPathsWorker?: import('jest-worker').default & {
    loadStaticPaths: typeof import('../../server/static-paths-worker').loadStaticPaths
  }
138

J
Joe Haddad 已提交
139 140 141 142
  public constructor({
    dir = '.',
    quiet = false,
    conf = null,
J
JJ Kasper 已提交
143
    dev = false,
144
    customServer = true,
J
Joe Haddad 已提交
145
  }: ServerConstructor = {}) {
N
nkzawa 已提交
146
    this.dir = resolve(dir)
N
Naoyuki Kanezawa 已提交
147
    this.quiet = quiet
T
Tim Neutkens 已提交
148
    const phase = this.currentPhase()
149
    loadEnvConfig(this.dir, dev)
150

151
    this.nextConfig = loadConfig(phase, this.dir, conf)
152
    this.distDir = join(this.dir, this.nextConfig.distDir)
153
    this.publicDir = join(this.dir, CLIENT_PUBLIC_FILES_PATH)
154
    this.hasStaticDir = fs.existsSync(join(this.dir, 'static'))
T
Tim Neutkens 已提交
155

156 157
    // Only serverRuntimeConfig needs the default
    // publicRuntimeConfig gets it's default in client/index.js
J
Joe Haddad 已提交
158 159 160 161 162
    const {
      serverRuntimeConfig = {},
      publicRuntimeConfig,
      assetPrefix,
      generateEtags,
163
      compress,
J
Joe Haddad 已提交
164
    } = this.nextConfig
165

T
Tim Neutkens 已提交
166
    this.buildId = this.readBuildId()
167

168
    this.renderOpts = {
T
Tim Neutkens 已提交
169
      poweredByHeader: this.nextConfig.poweredByHeader,
170
      canonicalBase: this.nextConfig.amp.canonicalBase,
171
      buildId: this.buildId,
172
      generateEtags,
173
      previewProps: this.getPreviewProps(),
174
      customServer: customServer === true ? true : undefined,
175
      ampOptimizerConfig: this.nextConfig.experimental.amp?.optimizer,
176
      basePath: this.nextConfig.experimental.basePath,
177
    }
N
Naoyuki Kanezawa 已提交
178

179 180
    // Only the `publicRuntimeConfig` key is exposed to the client side
    // It'll be rendered as part of __NEXT_DATA__ on the client side
181
    if (Object.keys(publicRuntimeConfig).length > 0) {
182
      this.renderOpts.runtimeConfig = publicRuntimeConfig
183 184
    }

185
    if (compress && this.nextConfig.target === 'server') {
186 187 188
      this.compression = compression() as Middleware
    }

189
    // Initialize next/config with the environment configuration
190 191 192 193
    envConfig.setConfig({
      serverRuntimeConfig,
      publicRuntimeConfig,
    })
194

195 196 197 198 199 200 201 202 203 204
    this.serverBuildDir = join(
      this.distDir,
      this._isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY
    )
    const pagesManifestPath = join(this.serverBuildDir, PAGES_MANIFEST)

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

J
JJ Kasper 已提交
205
    this.router = new Router(this.generateRoutes())
206
    this.setAssetPrefix(assetPrefix)
J
JJ Kasper 已提交
207

208 209 210
    // call init-server middleware, this is also handled
    // individually in serverless bundles when deployed
    if (!dev && this.nextConfig.experimental.plugins) {
211 212
      const initServer = require(join(this.serverBuildDir, 'init-server.js'))
        .default
213
      this.onErrorMiddleware = require(join(
214
        this.serverBuildDir,
215 216 217 218 219
        'on-error-server.js'
      )).default
      initServer()
    }

J
JJ Kasper 已提交
220 221 222 223 224 225 226 227 228 229 230 231
    initializeSprCache({
      dev,
      distDir: this.distDir,
      pagesDir: join(
        this.distDir,
        this._isLikeServerless
          ? SERVERLESS_DIRECTORY
          : `${SERVER_DIRECTORY}/static/${this.buildId}`,
        'pages'
      ),
      flushToDisk: this.nextConfig.experimental.sprFlushToDisk,
    })
N
Naoyuki Kanezawa 已提交
232
  }
N
nkzawa 已提交
233

234
  protected currentPhase(): string {
235
    return PHASE_PRODUCTION_SERVER
236 237
  }

238 239 240 241
  private logError(err: Error): void {
    if (this.onErrorMiddleware) {
      this.onErrorMiddleware({ err })
    }
242 243
    if (this.quiet) return
    // tslint:disable-next-line
244
    console.error(err)
245 246
  }

247
  private async handleRequest(
J
Joe Haddad 已提交
248 249
    req: IncomingMessage,
    res: ServerResponse,
250
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
251
  ): Promise<void> {
252
    // Parse url if parsedUrl not provided
253
    if (!parsedUrl || typeof parsedUrl !== 'object') {
254 255
      const url: any = req.url
      parsedUrl = parseUrl(url, true)
256
    }
257

258 259 260
    // 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 已提交
261
    }
262

263 264 265 266 267 268
    const { basePath } = this.nextConfig.experimental

    // if basePath is set require it be present
    if (basePath && !req.url!.startsWith(basePath)) {
      return this.render404(req, res, parsedUrl)
    } else {
T
Tim Neutkens 已提交
269
      // If replace ends up replacing the full url it'll be `undefined`, meaning we have to default it to `/`
270 271
      parsedUrl.pathname = parsedUrl.pathname!.replace(basePath, '') || '/'
      req.url = req.url!.replace(basePath, '')
T
Tim Neutkens 已提交
272 273
    }

274
    res.statusCode = 200
275 276 277
    try {
      return await this.run(req, res, parsedUrl)
    } catch (err) {
J
Joe Haddad 已提交
278 279 280
      this.logError(err)
      res.statusCode = 500
      res.end('Internal Server Error')
281
    }
282 283
  }

284
  public getRequestHandler() {
285
    return this.handleRequest.bind(this)
N
nkzawa 已提交
286 287
  }

288
  public setAssetPrefix(prefix?: string): void {
289
    this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
290 291
  }

292
  // Backwards compatibility
293
  public async prepare(): Promise<void> {}
N
nkzawa 已提交
294

T
Tim Neutkens 已提交
295
  // Backwards compatibility
296
  protected async close(): Promise<void> {}
T
Tim Neutkens 已提交
297

298
  protected setImmutableAssetCacheControl(res: ServerResponse): void {
T
Tim Neutkens 已提交
299
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
N
nkzawa 已提交
300 301
  }

J
JJ Kasper 已提交
302 303 304 305
  protected getCustomRoutes() {
    return require(join(this.distDir, ROUTES_MANIFEST))
  }

306 307 308 309
  private _cachedPreviewManifest: PrerenderManifest | undefined
  protected getPrerenderManifest(): PrerenderManifest {
    if (this._cachedPreviewManifest) {
      return this._cachedPreviewManifest
J
Joe Haddad 已提交
310
    }
311 312 313 314 315 316
    const manifest = require(join(this.distDir, PRERENDER_MANIFEST))
    return (this._cachedPreviewManifest = manifest)
  }

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

319
  protected generateRoutes(): {
320 321
    headers: Route[]
    rewrites: Route[]
322
    fsRoutes: Route[]
323
    redirects: Route[]
324 325
    catchAllRoute: Route
    pageChecker: PageChecker
326
    useFileSystemPublicRoutes: boolean
327 328
    dynamicRoutes: DynamicRoutes | undefined
  } {
J
JJ Kasper 已提交
329 330
    this.customRoutes = this.getCustomRoutes()

331 332 333
    const publicRoutes = fs.existsSync(this.publicDir)
      ? this.generatePublicRoutes()
      : []
J
JJ Kasper 已提交
334

335
    const staticFilesRoute = this.hasStaticDir
336 337 338 339 340
      ? [
          {
            // 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.
341
            // See more: https://github.com/vercel/next.js/issues/2617
342
            match: route('/static/:path*'),
343
            name: 'static catchall',
344
            fn: async (req, res, params, parsedUrl) => {
345 346 347 348 349
              const p = join(
                this.dir,
                'static',
                ...(params.path || []).map(encodeURIComponent)
              )
350
              await this.serveStatic(req, res, p, parsedUrl)
351 352 353
              return {
                finished: true,
              }
354 355 356 357
            },
          } as Route,
        ]
      : []
358

359 360 361 362
    let headers: Route[] = []
    let rewrites: Route[] = []
    let redirects: Route[] = []

363
    const fsRoutes: Route[] = [
T
Tim Neutkens 已提交
364
      {
365
        match: route('/_next/static/:path*'),
366 367
        type: 'route',
        name: '_next/static catchall',
368
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
369 370 371
          // The commons folder holds commonschunk files
          // The chunks folder holds dynamic entries
          // The buildId folder holds pages and potentially other assets. As buildId changes per build it can be long-term cached.
372 373

          // make sure to 404 for /_next/static itself
374 375 376 377 378 379
          if (!params.path) {
            await this.render404(req, res, parsedUrl)
            return {
              finished: true,
            }
          }
380

J
Joe Haddad 已提交
381 382 383
          if (
            params.path[0] === CLIENT_STATIC_FILES_RUNTIME ||
            params.path[0] === 'chunks' ||
384 385
            params.path[0] === 'css' ||
            params.path[0] === 'media' ||
J
Joe Haddad 已提交
386 387
            params.path[0] === this.buildId
          ) {
T
Tim Neutkens 已提交
388
            this.setImmutableAssetCacheControl(res)
389
          }
J
Joe Haddad 已提交
390 391 392
          const p = join(
            this.distDir,
            CLIENT_STATIC_FILES_PATH,
393
            ...(params.path || [])
J
Joe Haddad 已提交
394
          )
395
          await this.serveStatic(req, res, p, parsedUrl)
396 397 398
          return {
            finished: true,
          }
399
        },
400
      },
J
JJ Kasper 已提交
401 402
      {
        match: route('/_next/data/:path*'),
403 404
        type: 'route',
        name: '_next/data catchall',
J
JJ Kasper 已提交
405
        fn: async (req, res, params, _parsedUrl) => {
J
JJ Kasper 已提交
406 407 408
          // 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) {
409 410 411 412
            await this.render404(req, res, _parsedUrl)
            return {
              finished: true,
            }
J
JJ Kasper 已提交
413 414 415 416 417 418
          }
          // 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')) {
419 420 421 422
            await this.render404(req, res, _parsedUrl)
            return {
              finished: true,
            }
J
JJ Kasper 已提交
423 424 425
          }

          // re-create page's pathname
426 427 428 429 430
          const pathname = `/${params.path
            // we need to re-encode the params since they are decoded
            // by path-match and we are re-building the URL
            .map((param: string) => encodeURIComponent(param))
            .join('/')}`
J
JJ Kasper 已提交
431 432 433
            .replace(/\.json$/, '')
            .replace(/\/index$/, '/')

J
JJ Kasper 已提交
434
          const parsedUrl = parseUrl(pathname, true)
435

J
JJ Kasper 已提交
436 437 438 439
          await this.render(
            req,
            res,
            pathname,
440
            { ..._parsedUrl.query, _nextDataReq: '1' },
J
JJ Kasper 已提交
441 442
            parsedUrl
          )
443 444 445
          return {
            finished: true,
          }
J
JJ Kasper 已提交
446 447
        },
      },
T
Tim Neutkens 已提交
448
      {
449
        match: route('/_next/:path*'),
450 451
        type: 'route',
        name: '_next catchall',
T
Tim Neutkens 已提交
452
        // This path is needed because `render()` does a check for `/_next` and the calls the routing again
453
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
454
          await this.render404(req, res, parsedUrl)
455 456 457
          return {
            finished: true,
          }
L
Lukáš Huvar 已提交
458 459
        },
      },
460 461
      ...publicRoutes,
      ...staticFilesRoute,
T
Tim Neutkens 已提交
462
    ]
463

J
JJ Kasper 已提交
464 465
    if (this.customRoutes) {
      const getCustomRoute = (
466 467
        r: Rewrite | Redirect | Header,
        type: RouteType
468 469 470 471 472 473
      ) =>
        ({
          ...r,
          type,
          match: getCustomRouteMatcher(r.source),
          name: type,
T
Tim Neutkens 已提交
474
          fn: async (_req, _res, _params, _parsedUrl) => ({ finished: false }),
475
        } as Route & Rewrite & Header)
J
JJ Kasper 已提交
476

477 478 479 480 481 482 483 484 485 486
      const updateHeaderValue = (value: string, params: Params): string => {
        if (!value.includes(':')) {
          return value
        }
        const { parsedDestination } = prepareDestination(value, params, {})

        if (
          !parsedDestination.pathname ||
          !parsedDestination.pathname.startsWith('/')
        ) {
487 488 489 490 491
          // the value needs to start with a forward-slash to be compiled
          // correctly
          return compilePathToRegex(`/${value}`, { validate: false })(
            params
          ).substr(1)
492 493 494 495
        }
        return formatUrl(parsedDestination)
      }

496
      // Headers come very first
J
Joe Haddad 已提交
497
      headers = this.customRoutes.headers.map((r) => {
498
        const headerRoute = getCustomRoute(r, 'header')
499
        return {
500 501 502
          match: headerRoute.match,
          type: headerRoute.type,
          name: `${headerRoute.type} ${headerRoute.source} header route`,
503
          fn: async (_req, res, params, _parsedUrl) => {
504 505
            const hasParams = Object.keys(params).length > 0

506
            for (const header of (headerRoute as Header).headers) {
507
              let { key, value } = header
508 509 510
              if (hasParams) {
                key = updateHeaderValue(key, params)
                value = updateHeaderValue(value, params)
511 512
              }
              res.setHeader(key, value)
513 514 515 516 517
            }
            return { finished: false }
          },
        } as Route
      })
J
JJ Kasper 已提交
518

J
Joe Haddad 已提交
519
      redirects = this.customRoutes.redirects.map((redirect) => {
520
        const redirectRoute = getCustomRoute(redirect, 'redirect')
521
        return {
522 523 524
          type: redirectRoute.type,
          match: redirectRoute.match,
          statusCode: redirectRoute.statusCode,
525
          name: `Redirect route`,
526
          fn: async (_req, res, params, parsedUrl) => {
527
            const { parsedDestination } = prepareDestination(
528
              redirectRoute.destination,
529
              params,
530
              parsedUrl.query
531 532 533 534
            )
            const updatedDestination = formatUrl(parsedDestination)

            res.setHeader('Location', updatedDestination)
535
            res.statusCode = getRedirectStatus(redirectRoute as Redirect)
536 537 538 539 540 541

            // Since IE11 doesn't support the 308 header add backwards
            // compatibility using refresh header
            if (res.statusCode === 308) {
              res.setHeader('Refresh', `0;url=${updatedDestination}`)
            }
542

543 544 545 546 547 548 549
            res.end()
            return {
              finished: true,
            }
          },
        } as Route
      })
550

J
Joe Haddad 已提交
551
      rewrites = this.customRoutes.rewrites.map((rewrite) => {
552
        const rewriteRoute = getCustomRoute(rewrite, 'rewrite')
553 554
        return {
          check: true,
555
          type: rewriteRoute.type,
556
          name: `Rewrite route`,
557
          match: rewriteRoute.match,
558
          fn: async (req, res, params, parsedUrl) => {
559
            const { newUrl, parsedDestination } = prepareDestination(
560
              rewriteRoute.destination,
561
              params,
562 563
              parsedUrl.query,
              true
564 565 566 567 568 569 570 571 572
            )

            // external rewrite, proxy it
            if (parsedDestination.protocol) {
              const target = formatUrl(parsedDestination)
              const proxy = new Proxy({
                target,
                changeOrigin: true,
                ignorePath: true,
573
              })
574
              proxy.web(req, res)
575

576 577 578
              proxy.on('error', (err: Error) => {
                console.error(`Error occurred proxying ${target}`, err)
              })
579
              return {
580
                finished: true,
J
JJ Kasper 已提交
581
              }
582 583
            }
            ;(req as any)._nextDidRewrite = true
584
            ;(req as any)._nextRewroteUrl = newUrl
585

586 587 588 589 590 591 592 593
            return {
              finished: false,
              pathname: newUrl,
              query: parsedDestination.query,
            }
          },
        } as Route
      })
594 595 596 597 598 599 600 601 602 603 604 605
    }

    const catchAllRoute: Route = {
      match: route('/:path*'),
      type: 'route',
      name: 'Catchall render',
      fn: async (req, res, params, parsedUrl) => {
        const { pathname, query } = parsedUrl
        if (!pathname) {
          throw new Error('pathname is undefined')
        }

606
        if (params?.path?.[0] === 'api') {
607 608 609
          const handled = await this.handleApiRequest(
            req as NextApiRequest,
            res as NextApiResponse,
610
            pathname,
611
            query
612 613 614 615 616 617 618
          )
          if (handled) {
            return { finished: true }
          }
        }

        await this.render(req, res, pathname, query, parsedUrl)
619 620 621 622
        return {
          finished: true,
        }
      },
623
    }
624

625
    const { useFileSystemPublicRoutes } = this.nextConfig
J
Joe Haddad 已提交
626

627 628
    if (useFileSystemPublicRoutes) {
      this.dynamicRoutes = this.getDynamicRoutes()
629
    }
N
nkzawa 已提交
630

631
    return {
632
      headers,
633
      fsRoutes,
634 635
      rewrites,
      redirects,
636
      catchAllRoute,
637
      useFileSystemPublicRoutes,
638 639 640
      dynamicRoutes: this.dynamicRoutes,
      pageChecker: this.hasPage.bind(this),
    }
T
Tim Neutkens 已提交
641 642
  }

643
  private async getPagePath(pathname: string): Promise<string> {
644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660
    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
  }

661 662 663 664 665
  protected async _beforeCatchAllRender(
    _req: IncomingMessage,
    _res: ServerResponse,
    _params: Params,
    _parsedUrl: UrlWithParsedQuery
666
  ): Promise<boolean> {
667 668 669
    return false
  }

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

L
Lukáš Huvar 已提交
673 674 675 676 677 678
  /**
   * Resolves `API` request, in development builds on demand
   * @param req http request
   * @param res http response
   * @param pathname path of request
   */
J
Joe Haddad 已提交
679
  private async handleApiRequest(
680 681
    req: IncomingMessage,
    res: ServerResponse,
682 683
    pathname: string,
    query: ParsedUrlQuery
684
  ): Promise<boolean> {
685
    let page = pathname
L
Lukáš Huvar 已提交
686
    let params: Params | boolean = false
687
    let pageFound = await this.hasPage(page)
J
JJ Kasper 已提交
688

689
    if (!pageFound && this.dynamicRoutes) {
L
Lukáš Huvar 已提交
690 691
      for (const dynamicRoute of this.dynamicRoutes) {
        params = dynamicRoute.match(pathname)
692
        if (dynamicRoute.page.startsWith('/api') && params) {
693 694
          page = dynamicRoute.page
          pageFound = true
L
Lukáš Huvar 已提交
695 696 697 698 699
          break
        }
      }
    }

700
    if (!pageFound) {
701
      return false
J
JJ Kasper 已提交
702
    }
703 704 705 706
    // Make sure the page is built before getting the path
    // or else it won't be in the manifest yet
    await this.ensureApiPage(page)

707 708 709 710 711 712 713 714 715 716
    let builtPagePath
    try {
      builtPagePath = await this.getPagePath(page)
    } catch (err) {
      if (err.code === 'ENOENT') {
        return false
      }
      throw err
    }

717
    const pageModule = require(builtPagePath)
718
    query = { ...query, ...params }
J
JJ Kasper 已提交
719

720
    if (!this.renderOpts.dev && this._isLikeServerless) {
721
      if (typeof pageModule.default === 'function') {
722
        prepareServerlessUrl(req, query)
723 724
        await pageModule.default(req, res)
        return true
J
JJ Kasper 已提交
725 726 727
      }
    }

J
Joe Haddad 已提交
728 729 730 731 732
    await apiResolver(
      req,
      res,
      query,
      pageModule,
733
      this.renderOpts.previewProps,
734
      false,
J
Joe Haddad 已提交
735 736
      this.onErrorMiddleware
    )
737
    return true
L
Lukáš Huvar 已提交
738 739
  }

740
  protected generatePublicRoutes(): Route[] {
741
    const publicFiles = new Set(
J
Joe Haddad 已提交
742
      recursiveReadDirSync(this.publicDir).map((p) => p.replace(/\\/g, '/'))
743 744 745 746 747 748 749
    )

    return [
      {
        match: route('/:path*'),
        name: 'public folder catchall',
        fn: async (req, res, params, parsedUrl) => {
750 751
          const pathParts: string[] = params.path || []
          const path = `/${pathParts.join('/')}`
752 753 754 755 756 757

          if (publicFiles.has(path)) {
            await this.serveStatic(
              req,
              res,
              // we need to re-encode it since send decodes it
758
              join(this.publicDir, ...pathParts.map(encodeURIComponent)),
759 760
              parsedUrl
            )
761 762 763
            return {
              finished: true,
            }
764 765 766 767 768 769 770
          }
          return {
            finished: false,
          }
        },
      } as Route,
    ]
771 772
  }

773
  protected getDynamicRoutes() {
774 775
    return getSortedRoutes(Object.keys(this.pagesManifest!))
      .filter(isDynamicRoute)
J
Joe Haddad 已提交
776
      .map((page) => ({
777 778 779
        page,
        match: getRouteMatcher(getRouteRegex(page)),
      }))
J
Joe Haddad 已提交
780 781
  }

782
  private handleCompression(req: IncomingMessage, res: ServerResponse): void {
783 784 785 786 787
    if (this.compression) {
      this.compression(req, res, () => {})
    }
  }

788
  protected async run(
J
Joe Haddad 已提交
789 790
    req: IncomingMessage,
    res: ServerResponse,
791
    parsedUrl: UrlWithParsedQuery
792
  ): Promise<void> {
793 794
    this.handleCompression(req, res)

795
    try {
796 797
      const matched = await this.router.execute(req, res, parsedUrl)
      if (matched) {
798 799 800 801 802 803 804 805
        return
      }
    } catch (err) {
      if (err.code === 'DECODE_FAILED') {
        res.statusCode = 400
        return this.renderError(null, req, res, '/_error', {})
      }
      throw err
806 807
    }

808
    await this.render404(req, res, parsedUrl)
N
nkzawa 已提交
809 810
  }

811
  protected async sendHTML(
J
Joe Haddad 已提交
812 813
    req: IncomingMessage,
    res: ServerResponse,
814
    html: string
815
  ): Promise<void> {
T
Tim Neutkens 已提交
816 817
    const { generateEtags, poweredByHeader } = this.renderOpts
    return sendHTML(req, res, html, { generateEtags, poweredByHeader })
818 819
  }

J
Joe Haddad 已提交
820 821 822 823 824
  public async render(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
825
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
826
  ): Promise<void> {
827 828 829 830 831 832
    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`
      )
    }

833 834 835 836 837 838 839 840 841 842
    if (
      this.renderOpts.customServer &&
      pathname === '/index' &&
      !(await this.hasPage('/index'))
    ) {
      // maintain backwards compatibility for custom server
      // (see custom-server integration tests)
      pathname = '/'
    }

843
    const url: any = req.url
844

845 846 847 848
    // 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
849
    if (
850 851 852
      !query._nextDataReq &&
      (url.match(/^\/_next\//) ||
        (this.hasStaticDir && url.match(/^\/static\//)))
853
    ) {
854 855 856
      return this.handleRequest(req, res, parsedUrl)
    }

857
    if (isBlockedPage(pathname)) {
858
      return this.render404(req, res, parsedUrl)
859 860
    }

861
    const html = await this.renderToHTML(req, res, pathname, query)
862 863
    // Request was ended by the user
    if (html === null) {
864 865 866
      return
    }

867
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
868
  }
N
nkzawa 已提交
869

J
Joe Haddad 已提交
870
  private async findPageComponents(
J
Joe Haddad 已提交
871
    pathname: string,
872 873 874 875 876 877 878 879 880
    query: ParsedUrlQuery = {},
    params: Params | null = null
  ): Promise<FindComponentsResult | null> {
    const paths = [
      // try serving a static AMP version first
      query.amp ? normalizePagePath(pathname) + '.amp' : null,
      pathname,
    ].filter(Boolean)
    for (const pagePath of paths) {
J
JJ Kasper 已提交
881
      try {
882
        const components = await loadComponents(
J
Joe Haddad 已提交
883 884
          this.distDir,
          this.buildId,
885 886
          pagePath!,
          !this.renderOpts.dev && this._isLikeServerless
J
Joe Haddad 已提交
887
        )
888 889 890
        return {
          components,
          query: {
891
            ...(components.getStaticProps
892
              ? { _nextDataReq: query._nextDataReq, amp: query.amp }
893 894 895 896
              : query),
            ...(params || {}),
          },
        }
J
JJ Kasper 已提交
897 898 899 900
      } catch (err) {
        if (err.code !== 'ENOENT') throw err
      }
    }
901
    return null
J
Joe Haddad 已提交
902 903
  }

904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944
  private async getStaticPaths(
    pathname: string
  ): Promise<{
    staticPaths: string[] | undefined
    hasStaticFallback: boolean
  }> {
    // we lazy load the staticPaths to prevent the user
    // from waiting on them for the page to load in dev mode
    let staticPaths: string[] | undefined
    let hasStaticFallback = false

    if (!this.renderOpts.dev) {
      // `staticPaths` is intentionally set to `undefined` as it should've
      // been caught when checking disk data.
      staticPaths = undefined

      // Read whether or not fallback should exist from the manifest.
      hasStaticFallback =
        typeof this.getPrerenderManifest().dynamicRoutes[pathname].fallback ===
        'string'
    } else {
      const __getStaticPaths = async () => {
        const paths = await this.staticPathsWorker!.loadStaticPaths(
          this.distDir,
          this.buildId,
          pathname,
          !this.renderOpts.dev && this._isLikeServerless
        )
        return paths
      }
      ;({ paths: staticPaths, fallback: hasStaticFallback } = (
        await withCoalescedInvoke(__getStaticPaths)(
          `staticPaths-${pathname}`,
          []
        )
      ).value)
    }

    return { staticPaths, hasStaticFallback }
  }

J
Joe Haddad 已提交
945 946 947 948
  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
949
    { components, query }: FindComponentsResult,
950
    opts: RenderOptsPartial
951
  ): Promise<string | null> {
952
    // we need to ensure the status code if /404 is visited directly
953
    if (pathname === '/404') {
954 955 956
      res.statusCode = 404
    }

J
JJ Kasper 已提交
957
    // handle static page
958 959
    if (typeof components.Component === 'string') {
      return components.Component
J
Joe Haddad 已提交
960 961
    }

J
JJ Kasper 已提交
962 963
    // check request state
    const isLikeServerless =
964 965
      typeof components.Component === 'object' &&
      typeof (components.Component as any).renderReqToHTML === 'function'
966 967 968
    const isSSG = !!components.getStaticProps
    const isServerProps = !!components.getServerSideProps
    const hasStaticPaths = !!components.getStaticPaths
969

970 971 972 973
    if (!query.amp) {
      delete query.amp
    }

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

978 979 980 981 982 983 984 985
    let previewData: string | false | object | undefined
    let isPreviewMode = false

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

986 987 988 989 990 991
    // 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
    let urlPathname = (req as any)._nextRewroteUrl
      ? (req as any)._nextRewroteUrl
      : `${parseUrl(req.url || '').pathname!}`
992

993 994 995
    // remove trailing slash
    urlPathname = urlPathname.replace(/(?!^)\/$/, '')

996 997 998 999 1000 1001 1002 1003
    // remove /_next/data prefix from urlPathname so it matches
    // for direct page visit and /_next/data visit
    if (isDataReq && urlPathname.includes(this.buildId)) {
      urlPathname = (urlPathname.split(this.buildId).pop() || '/')
        .replace(/\.json$/, '')
        .replace(/\/index$/, '/')
    }

1004 1005 1006 1007
    const ssgCacheKey =
      isPreviewMode || !isSSG
        ? undefined // Preview mode bypasses the cache
        : `${urlPathname}${query.amp ? '.amp' : ''}`
J
JJ Kasper 已提交
1008 1009

    // Complete the response with cached data if its present
1010
    const cachedData = ssgCacheKey ? await getSprCache(ssgCacheKey) : undefined
1011

J
JJ Kasper 已提交
1012
    if (cachedData) {
1013
      const data = isDataReq
J
JJ Kasper 已提交
1014 1015 1016
        ? JSON.stringify(cachedData.pageData)
        : cachedData.html

1017
      sendPayload(
J
JJ Kasper 已提交
1018 1019
        res,
        data,
1020 1021 1022 1023 1024 1025 1026 1027 1028 1029
        isDataReq ? 'json' : 'html',
        !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,
            }
1030
          : undefined
J
JJ Kasper 已提交
1031 1032 1033 1034 1035 1036
      )

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

J
JJ Kasper 已提交
1039
    // If we're here, that means data is missing or it's stale.
1040 1041 1042 1043 1044 1045
    const maybeCoalesceInvoke = ssgCacheKey
      ? (fn: any) => withCoalescedInvoke(fn).bind(null, ssgCacheKey, [])
      : (fn: any) => async () => {
          const value = await fn()
          return { isOrigin: true, value }
        }
J
JJ Kasper 已提交
1046

J
Joe Haddad 已提交
1047
    const doRender = maybeCoalesceInvoke(async function (): Promise<{
J
JJ Kasper 已提交
1048
      html: string | null
1049
      pageData: any
J
JJ Kasper 已提交
1050 1051
      sprRevalidate: number | false
    }> {
1052
      let pageData: any
J
JJ Kasper 已提交
1053 1054 1055 1056 1057 1058
      let html: string | null
      let sprRevalidate: number | false

      let renderResult
      // handle serverless
      if (isLikeServerless) {
1059
        renderResult = await (components.Component as any).renderReqToHTML(
1060 1061
          req,
          res,
1062
          'passthrough'
1063
        )
J
JJ Kasper 已提交
1064 1065

        html = renderResult.html
1066
        pageData = renderResult.renderOpts.pageData
J
JJ Kasper 已提交
1067 1068
        sprRevalidate = renderResult.renderOpts.revalidate
      } else {
1069
        const renderOpts: RenderOpts = {
1070
          ...components,
J
JJ Kasper 已提交
1071
          ...opts,
1072
          isDataReq,
J
JJ Kasper 已提交
1073 1074 1075 1076
        }
        renderResult = await renderToHTML(req, res, pathname, query, renderOpts)

        html = renderResult
1077 1078 1079
        // TODO: change this to a different passing mechanism
        pageData = (renderOpts as any).pageData
        sprRevalidate = (renderOpts as any).revalidate
J
JJ Kasper 已提交
1080 1081
      }

1082
      return { html, pageData, sprRevalidate }
1083
    })
J
JJ Kasper 已提交
1084

1085
    const isProduction = !this.renderOpts.dev
J
Joe Haddad 已提交
1086
    const isDynamicPathname = isDynamicRoute(pathname)
1087
    const didRespond = isResSent(res)
1088

1089 1090 1091
    const { staticPaths, hasStaticFallback } = hasStaticPaths
      ? await this.getStaticPaths(pathname)
      : { staticPaths: undefined, hasStaticFallback: false }
1092

1093 1094 1095 1096 1097 1098 1099 1100 1101 1102
    // const isForcedBlocking =
    //   req.headers['X-Prerender-Bypass-Mode'] !== 'Blocking'

    // When we did not respond from cache, we need to choose to block on
    // rendering or return a skeleton.
    //
    // * Data requests always block.
    //
    // * Preview mode toggles all pages to be resolved in a blocking manner.
    //
1103
    // * Non-dynamic pages should block (though this is an impossible
1104 1105
    //   case in production).
    //
1106 1107
    // * Dynamic pages should return their skeleton if not defined in
    //   getStaticPaths, then finish the data request on the client-side.
1108
    //
J
Joe Haddad 已提交
1109
    if (
1110
      ssgCacheKey &&
1111
      !didRespond &&
J
Joe Haddad 已提交
1112
      !isDataReq &&
1113 1114
      !isPreviewMode &&
      isDynamicPathname &&
1115 1116 1117
      // Development should trigger fallback when the path is not in
      // `getStaticPaths`
      (isProduction || !staticPaths || !staticPaths.includes(urlPathname))
J
Joe Haddad 已提交
1118
    ) {
1119 1120 1121 1122 1123 1124 1125
      if (
        // In development, fall through to render to handle missing
        // getStaticPaths.
        (isProduction || staticPaths) &&
        // When fallback isn't present, abort this render so we 404
        !hasStaticFallback
      ) {
1126
        throw new NoFallbackError()
1127 1128
      }

1129
      let html: string
1130

1131 1132
      // Production already emitted the fallback as static HTML.
      if (isProduction) {
1133
        html = await getFallback(pathname)
1134 1135 1136
      }
      // We need to generate the fallback on-demand for development.
      else {
1137 1138
        query.__nextFallback = 'true'
        if (isLikeServerless) {
1139
          prepareServerlessUrl(req, query)
1140
        }
1141 1142
        const { value: renderResult } = await doRender()
        html = renderResult.html
1143 1144
      }

1145
      sendPayload(res, html, 'html')
1146
      return null
1147 1148
    }

1149 1150 1151
    const {
      isOrigin,
      value: { html, pageData, sprRevalidate },
1152
    } = await doRender()
1153 1154
    let resHtml = html
    if (!isResSent(res) && (isSSG || isDataReq || isServerProps)) {
1155
      sendPayload(
1156 1157
        res,
        isDataReq ? JSON.stringify(pageData) : html,
1158
        isDataReq ? 'json' : 'html',
1159
        !this.renderOpts.dev || (isServerProps && !isDataReq)
1160 1161
          ? {
              private: isPreviewMode,
1162
              stateful: !isSSG,
1163 1164
              revalidate: sprRevalidate,
            }
1165
          : undefined
1166
      )
1167
      resHtml = null
1168
    }
J
JJ Kasper 已提交
1169

1170 1171 1172
    // Update the SPR cache if the head request and cacheable
    if (isOrigin && ssgCacheKey) {
      await setSprCache(ssgCacheKey, { html: html!, pageData }, sprRevalidate)
1173 1174
    }

1175
    return resHtml
1176 1177
  }

1178
  public async renderToHTML(
J
Joe Haddad 已提交
1179 1180 1181
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1182
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1183
  ): Promise<string | null> {
1184 1185 1186
    try {
      const result = await this.findPageComponents(pathname, query)
      if (result) {
1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198
        try {
          return await this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
            result,
            { ...this.renderOpts }
          )
        } catch (err) {
          if (!(err instanceof NoFallbackError)) {
            throw err
          }
1199
        }
1200
      }
J
Joe Haddad 已提交
1201

1202 1203 1204 1205 1206 1207
      if (this.dynamicRoutes) {
        for (const dynamicRoute of this.dynamicRoutes) {
          const params = dynamicRoute.match(pathname)
          if (!params) {
            continue
          }
J
Joe Haddad 已提交
1208

1209
          const dynamicRouteResult = await this.findPageComponents(
1210 1211 1212 1213
            dynamicRoute.page,
            query,
            params
          )
1214
          if (dynamicRouteResult) {
1215 1216 1217 1218 1219
            try {
              return await this.renderToHTMLWithComponents(
                req,
                res,
                dynamicRoute.page,
1220
                dynamicRouteResult,
1221 1222 1223 1224 1225 1226
                { ...this.renderOpts, params }
              )
            } catch (err) {
              if (!(err instanceof NoFallbackError)) {
                throw err
              }
1227
            }
J
Joe Haddad 已提交
1228 1229
          }
        }
1230 1231 1232 1233 1234 1235 1236 1237 1238
      }
    } catch (err) {
      this.logError(err)
      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 已提交
1239 1240
  }

J
Joe Haddad 已提交
1241 1242 1243 1244 1245
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1246
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1247 1248 1249
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
1250
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
1251
    )
N
Naoyuki Kanezawa 已提交
1252
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
1253
    if (html === null) {
1254 1255
      return
    }
1256
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
1257 1258
  }

1259 1260 1261 1262 1263 1264 1265 1266 1267
  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 已提交
1268 1269 1270 1271 1272
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
1273
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1274
  ) {
1275
    let result: null | FindComponentsResult = null
1276

1277 1278 1279
    const is404 = res.statusCode === 404
    let using404Page = false

1280
    // use static 404 page if available and is 404 response
1281
    if (is404) {
1282 1283
      result = await this.findPageComponents('/404')
      using404Page = result !== null
1284 1285 1286 1287 1288 1289
    }

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

1290 1291 1292
    if (
      process.env.NODE_ENV !== 'production' &&
      !using404Page &&
1293 1294
      (await this.hasPage('/_error')) &&
      !(await this.hasPage('/404'))
1295 1296 1297 1298
    ) {
      this.customErrorNo404Warn()
    }

1299
    let html: string | null
1300
    try {
1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311
      try {
        html = await this.renderToHTMLWithComponents(
          req,
          res,
          using404Page ? '/404' : '/_error',
          result!,
          {
            ...this.renderOpts,
            err,
          }
        )
1312 1313
      } catch (maybeFallbackError) {
        if (maybeFallbackError instanceof NoFallbackError) {
1314
          throw new Error('invariant: failed to render error page')
1315
        }
1316
        throw maybeFallbackError
1317
      }
1318 1319
    } catch (renderToHtmlError) {
      console.error(renderToHtmlError)
1320 1321 1322 1323
      res.statusCode = 500
      html = 'Internal Server Error'
    }
    return html
N
Naoyuki Kanezawa 已提交
1324 1325
  }

J
Joe Haddad 已提交
1326 1327 1328
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
1329
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1330
  ): Promise<void> {
1331 1332
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
N
Naoyuki Kanezawa 已提交
1333
    res.statusCode = 404
1334
    return this.renderError(null, req, res, pathname!, query)
N
Naoyuki Kanezawa 已提交
1335
  }
N
Naoyuki Kanezawa 已提交
1336

J
Joe Haddad 已提交
1337 1338 1339 1340
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
1341
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1342
  ): Promise<void> {
A
Arunoda Susiripala 已提交
1343
    if (!this.isServeableUrl(path)) {
1344
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
1345 1346
    }

1347 1348 1349 1350 1351 1352
    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 已提交
1353
    try {
1354
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
1355
    } catch (err) {
T
Tim Neutkens 已提交
1356
      if (err.code === 'ENOENT' || err.statusCode === 404) {
1357
        this.render404(req, res, parsedUrl)
1358 1359 1360
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
1361 1362 1363 1364 1365 1366
      } else {
        throw err
      }
    }
  }

1367 1368 1369 1370 1371 1372 1373 1374 1375
  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 已提交
1376
      userFilesStatic = recursiveReadDirSync(pathUserFilesStatic).map((f) =>
1377 1378 1379 1380 1381 1382
        join('.', 'static', f)
      )
    }

    let userFilesPublic: string[] = []
    if (this.publicDir && fs.existsSync(this.publicDir)) {
J
Joe Haddad 已提交
1383
      userFilesPublic = recursiveReadDirSync(this.publicDir).map((f) =>
1384 1385 1386 1387 1388 1389 1390
        join('.', 'public', f)
      )
    }

    let nextFilesStatic: string[] = []
    nextFilesStatic = recursiveReadDirSync(
      join(this.distDir, 'static')
J
Joe Haddad 已提交
1391
    ).map((f) => join('.', relative(this.dir, this.distDir), 'static', f))
1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425

    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 已提交
1426
    if (
1427 1428 1429
      (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 已提交
1430 1431 1432 1433
    ) {
      return false
    }

1434 1435 1436 1437
    // Check against the real filesystem paths
    const filesystemUrls = this.getFilesystemPaths()
    const resolved = relative(this.dir, untrustedFilePath)
    return filesystemUrls.has(resolved)
A
Arunoda Susiripala 已提交
1438 1439
  }

1440
  protected readBuildId(): string {
1441 1442 1443 1444 1445
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
1446
        throw new Error(
1447
          `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 已提交
1448
        )
1449 1450 1451
      }

      throw err
1452
    }
1453
  }
1454 1455 1456 1457

  private get _isLikeServerless(): boolean {
    return isTargetLikeServerless(this.nextConfig.target)
  }
1458
}
1459

1460 1461 1462 1463
function prepareServerlessUrl(
  req: IncomingMessage,
  query: ParsedUrlQuery
): void {
1464 1465 1466 1467 1468 1469 1470 1471 1472 1473
  const curUrl = parseUrl(req.url!, true)
  req.url = formatUrl({
    ...curUrl,
    search: undefined,
    query: {
      ...curUrl.query,
      ...query,
    },
  })
}
1474 1475

class NoFallbackError extends Error {}