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

const getCustomRouteMatcher = pathMatch(true)
69 70 71

type NextConfig = any

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

78 79 80 81 82
type FindComponentsResult = {
  components: LoadComponentsReturnType
  query: ParsedUrlQuery
}

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

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

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

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

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

T
Tim Neutkens 已提交
169
    this.buildId = this.readBuildId()
170

171
    this.renderOpts = {
T
Tim Neutkens 已提交
172
      poweredByHeader: this.nextConfig.poweredByHeader,
173
      canonicalBase: this.nextConfig.amp.canonicalBase,
174 175
      documentMiddlewareEnabled: this.nextConfig.experimental
        .documentMiddleware,
J
Joe Haddad 已提交
176
      hasCssMode: this.nextConfig.experimental.css,
177
      staticMarkup,
178
      buildId: this.buildId,
179
      generateEtags,
180
      previewProps: this.getPreviewProps(),
181
      customServer: customServer === true ? true : undefined,
182
      ampOptimizerConfig: this.nextConfig.experimental.amp?.optimizer,
183
      basePath: this.nextConfig.experimental.basePath,
184
    }
N
Naoyuki Kanezawa 已提交
185

186 187
    // Only the `publicRuntimeConfig` key is exposed to the client side
    // It'll be rendered as part of __NEXT_DATA__ on the client side
188
    if (Object.keys(publicRuntimeConfig).length > 0) {
189
      this.renderOpts.runtimeConfig = publicRuntimeConfig
190 191
    }

192
    if (compress && this.nextConfig.target === 'server') {
193 194 195
      this.compression = compression() as Middleware
    }

196
    // Initialize next/config with the environment configuration
197 198 199 200
    envConfig.setConfig({
      serverRuntimeConfig,
      publicRuntimeConfig,
    })
201

202 203 204 205 206 207 208 209 210 211
    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 已提交
212
    this.router = new Router(this.generateRoutes())
213
    this.setAssetPrefix(assetPrefix)
J
JJ Kasper 已提交
214

215 216 217
    // call init-server middleware, this is also handled
    // individually in serverless bundles when deployed
    if (!dev && this.nextConfig.experimental.plugins) {
218 219
      const initServer = require(join(this.serverBuildDir, 'init-server.js'))
        .default
220
      this.onErrorMiddleware = require(join(
221
        this.serverBuildDir,
222 223 224 225 226
        'on-error-server.js'
      )).default
      initServer()
    }

J
JJ Kasper 已提交
227 228 229 230 231 232 233 234 235 236 237 238
    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 已提交
239
  }
N
nkzawa 已提交
240

241
  protected currentPhase(): string {
242
    return PHASE_PRODUCTION_SERVER
243 244
  }

245 246 247 248
  private logError(err: Error): void {
    if (this.onErrorMiddleware) {
      this.onErrorMiddleware({ err })
    }
249 250
    if (this.quiet) return
    // tslint:disable-next-line
251
    console.error(err)
252 253
  }

254
  private async handleRequest(
J
Joe Haddad 已提交
255 256
    req: IncomingMessage,
    res: ServerResponse,
257
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
258
  ): Promise<void> {
259
    // Parse url if parsedUrl not provided
260
    if (!parsedUrl || typeof parsedUrl !== 'object') {
261 262
      const url: any = req.url
      parsedUrl = parseUrl(url, true)
263
    }
264

265 266 267
    // 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 已提交
268
    }
269

270 271 272 273 274 275
    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 已提交
276
      // If replace ends up replacing the full url it'll be `undefined`, meaning we have to default it to `/`
277 278
      parsedUrl.pathname = parsedUrl.pathname!.replace(basePath, '') || '/'
      req.url = req.url!.replace(basePath, '')
T
Tim Neutkens 已提交
279 280
    }

281
    res.statusCode = 200
282 283 284
    try {
      return await this.run(req, res, parsedUrl)
    } catch (err) {
J
Joe Haddad 已提交
285 286 287
      this.logError(err)
      res.statusCode = 500
      res.end('Internal Server Error')
288
    }
289 290
  }

291
  public getRequestHandler() {
292
    return this.handleRequest.bind(this)
N
nkzawa 已提交
293 294
  }

295
  public setAssetPrefix(prefix?: string) {
296
    this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
297 298
  }

299
  // Backwards compatibility
300
  public async prepare(): Promise<void> {}
N
nkzawa 已提交
301

T
Tim Neutkens 已提交
302
  // Backwards compatibility
303
  protected async close(): Promise<void> {}
T
Tim Neutkens 已提交
304

305
  protected setImmutableAssetCacheControl(res: ServerResponse) {
T
Tim Neutkens 已提交
306
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
N
nkzawa 已提交
307 308
  }

J
JJ Kasper 已提交
309 310 311 312
  protected getCustomRoutes() {
    return require(join(this.distDir, ROUTES_MANIFEST))
  }

313 314 315 316
  private _cachedPreviewManifest: PrerenderManifest | undefined
  protected getPrerenderManifest(): PrerenderManifest {
    if (this._cachedPreviewManifest) {
      return this._cachedPreviewManifest
J
Joe Haddad 已提交
317
    }
318 319 320 321 322 323
    const manifest = require(join(this.distDir, PRERENDER_MANIFEST))
    return (this._cachedPreviewManifest = manifest)
  }

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

326
  protected generateRoutes(): {
327 328
    headers: Route[]
    rewrites: Route[]
329
    fsRoutes: Route[]
330
    redirects: Route[]
331 332
    catchAllRoute: Route
    pageChecker: PageChecker
333
    useFileSystemPublicRoutes: boolean
334 335
    dynamicRoutes: DynamicRoutes | undefined
  } {
J
JJ Kasper 已提交
336 337
    this.customRoutes = this.getCustomRoutes()

338 339 340
    const publicRoutes = fs.existsSync(this.publicDir)
      ? this.generatePublicRoutes()
      : []
J
JJ Kasper 已提交
341

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

366 367 368 369
    let headers: Route[] = []
    let rewrites: Route[] = []
    let redirects: Route[] = []

370
    const fsRoutes: Route[] = [
T
Tim Neutkens 已提交
371
      {
372
        match: route('/_next/static/:path*'),
373 374
        type: 'route',
        name: '_next/static catchall',
375
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
376 377 378
          // 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.
379 380

          // make sure to 404 for /_next/static itself
381 382 383 384 385 386
          if (!params.path) {
            await this.render404(req, res, parsedUrl)
            return {
              finished: true,
            }
          }
387

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

          // re-create page's pathname
          const pathname = `/${params.path.join('/')}`
            .replace(/\.json$/, '')
            .replace(/\/index$/, '/')

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

J
JJ Kasper 已提交
466 467
    if (this.customRoutes) {
      const getCustomRoute = (
468 469
        r: Rewrite | Redirect | Header,
        type: RouteType
470 471 472 473 474 475 476 477
      ) =>
        ({
          ...r,
          type,
          match: getCustomRouteMatcher(r.source),
          name: type,
          fn: async (req, res, params, parsedUrl) => ({ finished: false }),
        } as Route & Rewrite & Header)
J
JJ Kasper 已提交
478

479
      // Headers come very first
480 481 482 483 484 485 486 487 488 489 490 491 492 493
      headers = this.customRoutes.headers.map(r => {
        const route = getCustomRoute(r, 'header')
        return {
          match: route.match,
          type: route.type,
          name: `${route.type} ${route.source} header route`,
          fn: async (_req, res, _params, _parsedUrl) => {
            for (const header of (route as Header).headers) {
              res.setHeader(header.key, header.value)
            }
            return { finished: false }
          },
        } as Route
      })
J
JJ Kasper 已提交
494

495 496 497 498 499 500 501
      redirects = this.customRoutes.redirects.map(redirect => {
        const route = getCustomRoute(redirect, 'redirect')
        return {
          type: route.type,
          match: route.match,
          statusCode: route.statusCode,
          name: `Redirect route`,
502
          fn: async (_req, res, params, parsedUrl) => {
503 504
            const { parsedDestination } = prepareDestination(
              route.destination,
505
              params,
506
              parsedUrl.query,
507
              true
508 509 510 511 512 513 514 515 516 517 518
            )
            const updatedDestination = formatUrl(parsedDestination)

            res.setHeader('Location', updatedDestination)
            res.statusCode = getRedirectStatus(route 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}`)
            }
519

520 521 522 523 524 525 526
            res.end()
            return {
              finished: true,
            }
          },
        } as Route
      })
527

528 529 530 531 532 533 534
      rewrites = this.customRoutes.rewrites.map(rewrite => {
        const route = getCustomRoute(rewrite, 'rewrite')
        return {
          check: true,
          type: route.type,
          name: `Rewrite route`,
          match: route.match,
535
          fn: async (req, res, params, parsedUrl) => {
536 537
            const { newUrl, parsedDestination } = prepareDestination(
              route.destination,
538 539
              params,
              parsedUrl.query
540 541 542 543 544 545 546 547 548
            )

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

552 553 554
              proxy.on('error', (err: Error) => {
                console.error(`Error occurred proxying ${target}`, err)
              })
555
              return {
556
                finished: true,
J
JJ Kasper 已提交
557
              }
558 559
            }
            ;(req as any)._nextDidRewrite = true
560

561 562 563 564 565 566 567 568
            return {
              finished: false,
              pathname: newUrl,
              query: parsedDestination.query,
            }
          },
        } as Route
      })
569 570 571 572 573 574 575 576 577 578 579 580
    }

    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')
        }

581
        if (params?.path?.[0] === 'api') {
582 583 584
          const handled = await this.handleApiRequest(
            req as NextApiRequest,
            res as NextApiResponse,
585 586
            pathname!,
            query
587 588 589 590 591 592 593
          )
          if (handled) {
            return { finished: true }
          }
        }

        await this.render(req, res, pathname, query, parsedUrl)
594 595 596 597
        return {
          finished: true,
        }
      },
598
    }
599

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

602 603
    if (useFileSystemPublicRoutes) {
      this.dynamicRoutes = this.getDynamicRoutes()
604
    }
N
nkzawa 已提交
605

606
    return {
607
      headers,
608
      fsRoutes,
609 610
      rewrites,
      redirects,
611
      catchAllRoute,
612
      useFileSystemPublicRoutes,
613 614 615
      dynamicRoutes: this.dynamicRoutes,
      pageChecker: this.hasPage.bind(this),
    }
T
Tim Neutkens 已提交
616 617
  }

618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635
  private async getPagePath(pathname: string) {
    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
  }

636 637 638 639 640 641 642 643 644
  protected async _beforeCatchAllRender(
    _req: IncomingMessage,
    _res: ServerResponse,
    _params: Params,
    _parsedUrl: UrlWithParsedQuery
  ) {
    return false
  }

645 646 647
  // Used to build API page in development
  protected async ensureApiPage(pathname: string) {}

L
Lukáš Huvar 已提交
648 649 650 651 652 653
  /**
   * Resolves `API` request, in development builds on demand
   * @param req http request
   * @param res http response
   * @param pathname path of request
   */
J
Joe Haddad 已提交
654
  private async handleApiRequest(
655 656
    req: IncomingMessage,
    res: ServerResponse,
657 658
    pathname: string,
    query: ParsedUrlQuery
J
Joe Haddad 已提交
659
  ) {
660
    let page = pathname
L
Lukáš Huvar 已提交
661
    let params: Params | boolean = false
662
    let pageFound = await this.hasPage(page)
J
JJ Kasper 已提交
663

664
    if (!pageFound && this.dynamicRoutes) {
L
Lukáš Huvar 已提交
665 666
      for (const dynamicRoute of this.dynamicRoutes) {
        params = dynamicRoute.match(pathname)
667
        if (dynamicRoute.page.startsWith('/api') && params) {
668 669
          page = dynamicRoute.page
          pageFound = true
L
Lukáš Huvar 已提交
670 671 672 673 674
          break
        }
      }
    }

675
    if (!pageFound) {
676
      return false
J
JJ Kasper 已提交
677
    }
678 679 680 681 682 683
    // Make sure the page is built before getting the path
    // or else it won't be in the manifest yet
    await this.ensureApiPage(page)

    const builtPagePath = await this.getPagePath(page)
    const pageModule = require(builtPagePath)
684
    query = { ...query, ...params }
J
JJ Kasper 已提交
685

686
    if (!this.renderOpts.dev && this._isLikeServerless) {
687
      if (typeof pageModule.default === 'function') {
688
        prepareServerlessUrl(req, query)
689 690
        await pageModule.default(req, res)
        return true
J
JJ Kasper 已提交
691 692 693
      }
    }

J
Joe Haddad 已提交
694 695 696 697 698
    await apiResolver(
      req,
      res,
      query,
      pageModule,
699
      this.renderOpts.previewProps,
J
Joe Haddad 已提交
700 701
      this.onErrorMiddleware
    )
702
    return true
L
Lukáš Huvar 已提交
703 704
  }

705
  protected generatePublicRoutes(): Route[] {
706 707 708 709 710 711 712 713 714
    const publicFiles = new Set(
      recursiveReadDirSync(this.publicDir).map(p => p.replace(/\\/g, '/'))
    )

    return [
      {
        match: route('/:path*'),
        name: 'public folder catchall',
        fn: async (req, res, params, parsedUrl) => {
715 716
          const pathParts: string[] = params.path || []
          const path = `/${pathParts.join('/')}`
717 718 719 720 721 722

          if (publicFiles.has(path)) {
            await this.serveStatic(
              req,
              res,
              // we need to re-encode it since send decodes it
723
              join(this.publicDir, ...pathParts.map(encodeURIComponent)),
724 725
              parsedUrl
            )
726 727 728
            return {
              finished: true,
            }
729 730 731 732 733 734 735
          }
          return {
            finished: false,
          }
        },
      } as Route,
    ]
736 737
  }

738
  protected getDynamicRoutes() {
739 740 741
    const dynamicRoutedPages = Object.keys(this.pagesManifest!).filter(
      isDynamicRoute
    )
742 743 744 745
    return getSortedRoutes(dynamicRoutedPages).map(page => ({
      page,
      match: getRouteMatcher(getRouteRegex(page)),
    }))
J
Joe Haddad 已提交
746 747
  }

748 749 750 751 752 753
  private handleCompression(req: IncomingMessage, res: ServerResponse) {
    if (this.compression) {
      this.compression(req, res, () => {})
    }
  }

754
  protected async run(
J
Joe Haddad 已提交
755 756
    req: IncomingMessage,
    res: ServerResponse,
757
    parsedUrl: UrlWithParsedQuery
J
Joe Haddad 已提交
758
  ) {
759 760
    this.handleCompression(req, res)

761
    try {
762 763
      const matched = await this.router.execute(req, res, parsedUrl)
      if (matched) {
764 765 766 767 768 769 770 771
        return
      }
    } catch (err) {
      if (err.code === 'DECODE_FAILED') {
        res.statusCode = 400
        return this.renderError(null, req, res, '/_error', {})
      }
      throw err
772 773
    }

774
    await this.render404(req, res, parsedUrl)
N
nkzawa 已提交
775 776
  }

777
  protected async sendHTML(
J
Joe Haddad 已提交
778 779
    req: IncomingMessage,
    res: ServerResponse,
780
    html: string
J
Joe Haddad 已提交
781
  ) {
T
Tim Neutkens 已提交
782 783
    const { generateEtags, poweredByHeader } = this.renderOpts
    return sendHTML(req, res, html, { generateEtags, poweredByHeader })
784 785
  }

J
Joe Haddad 已提交
786 787 788 789 790
  public async render(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
791
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
792
  ): Promise<void> {
793 794 795 796 797 798
    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`
      )
    }

799
    const url: any = req.url
800

801 802 803 804
    // 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
805
    if (
806 807 808
      !query._nextDataReq &&
      (url.match(/^\/_next\//) ||
        (this.hasStaticDir && url.match(/^\/static\//)))
809
    ) {
810 811 812
      return this.handleRequest(req, res, parsedUrl)
    }

813
    if (isBlockedPage(pathname)) {
814
      return this.render404(req, res, parsedUrl)
815 816
    }

817
    const html = await this.renderToHTML(req, res, pathname, query)
818 819
    // Request was ended by the user
    if (html === null) {
820 821 822
      return
    }

823
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
824
  }
N
nkzawa 已提交
825

J
Joe Haddad 已提交
826
  private async findPageComponents(
J
Joe Haddad 已提交
827
    pathname: string,
828 829 830 831 832 833 834 835 836
    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 已提交
837
      try {
838
        const components = await loadComponents(
J
Joe Haddad 已提交
839 840
          this.distDir,
          this.buildId,
841 842
          pagePath!,
          !this.renderOpts.dev && this._isLikeServerless
J
Joe Haddad 已提交
843
        )
844 845 846
        return {
          components,
          query: {
847
            ...(components.getStaticProps
848
              ? { _nextDataReq: query._nextDataReq, amp: query.amp }
849 850 851 852
              : query),
            ...(params || {}),
          },
        }
J
JJ Kasper 已提交
853 854 855 856
      } catch (err) {
        if (err.code !== 'ENOENT') throw err
      }
    }
857
    return null
J
Joe Haddad 已提交
858 859
  }

860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900
  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 已提交
901 902 903 904
  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
905
    { components, query }: FindComponentsResult,
906
    opts: RenderOptsPartial
907
  ): Promise<string | null> {
908
    // we need to ensure the status code if /404 is visited directly
909
    if (pathname === '/404') {
910 911 912
      res.statusCode = 404
    }

J
JJ Kasper 已提交
913
    // handle static page
914 915
    if (typeof components.Component === 'string') {
      return components.Component
J
Joe Haddad 已提交
916 917
    }

J
JJ Kasper 已提交
918 919
    // check request state
    const isLikeServerless =
920 921
      typeof components.Component === 'object' &&
      typeof (components.Component as any).renderReqToHTML === 'function'
922 923 924
    const isSSG = !!components.getStaticProps
    const isServerProps = !!components.getServerSideProps
    const hasStaticPaths = !!components.getStaticPaths
925

926 927 928 929 930 931 932 933
    if (isSSG && query.amp) {
      pathname += `.amp`
    }

    if (!query.amp) {
      delete query.amp
    }

934
    // Toggle whether or not this is a Data request
935
    const isDataReq = !!query._nextDataReq
936 937
    delete query._nextDataReq

938 939 940 941 942 943 944 945
    let previewData: string | false | object | undefined
    let isPreviewMode = false

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

J
JJ Kasper 已提交
946
    // non-spr requests should render like normal
947
    if (!isSSG) {
J
JJ Kasper 已提交
948 949
      // handle serverless
      if (isLikeServerless) {
950
        if (isDataReq) {
951
          const renderResult = await (components.Component as any).renderReqToHTML(
952 953
            req,
            res,
954
            'passthrough'
955 956
          )

957
          sendPayload(
958 959
            res,
            JSON.stringify(renderResult?.renderOpts?.pageData),
960
            'json',
961 962
            !this.renderOpts.dev
              ? {
963 964
                  private: isPreviewMode,
                  stateful: true, // non-SSG data request
965 966
                }
              : undefined
967 968 969
          )
          return null
        }
970
        prepareServerlessUrl(req, query)
971
        return (components.Component as any).renderReqToHTML(req, res)
J
JJ Kasper 已提交
972 973
      }

974 975
      if (isDataReq && isServerProps) {
        const props = await renderToHTML(req, res, pathname, query, {
976
          ...components,
977 978 979
          ...opts,
          isDataReq,
        })
980 981 982
        sendPayload(
          res,
          JSON.stringify(props),
983
          'json',
984 985
          !this.renderOpts.dev
            ? {
986 987
                private: isPreviewMode,
                stateful: true, // GSSP data request
988 989 990
              }
            : undefined
        )
991 992 993
        return null
      }

994
      const html = await renderToHTML(req, res, pathname, query, {
995
        ...components,
J
JJ Kasper 已提交
996 997 998
        ...opts,
      })

999 1000
      if (html && isServerProps) {
        sendPayload(res, html, 'html', {
1001
          private: isPreviewMode,
1002
          stateful: true, // GSSP request
1003
        })
1004
        return null
1005 1006 1007 1008
      }

      return html
    }
J
Joe Haddad 已提交
1009

1010 1011
    // Compute the iSSG cache key
    let urlPathname = `${parseUrl(req.url || '').pathname!}${
1012 1013
      query.amp ? '.amp' : ''
    }`
1014 1015 1016 1017 1018 1019 1020 1021 1022

    // 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$/, '/')
    }

J
Joe Haddad 已提交
1023 1024
    const ssgCacheKey = isPreviewMode
      ? `__` + nanoid() // Preview mode uses a throw away key to not coalesce preview invokes
1025
      : urlPathname
J
JJ Kasper 已提交
1026 1027

    // Complete the response with cached data if its present
J
Joe Haddad 已提交
1028 1029 1030 1031
    const cachedData = isPreviewMode
      ? // Preview data bypasses the cache
        undefined
      : await getSprCache(ssgCacheKey)
J
JJ Kasper 已提交
1032
    if (cachedData) {
1033
      const data = isDataReq
J
JJ Kasper 已提交
1034 1035 1036
        ? JSON.stringify(cachedData.pageData)
        : cachedData.html

1037
      sendPayload(
J
JJ Kasper 已提交
1038 1039
        res,
        data,
1040 1041 1042 1043 1044 1045 1046 1047 1048 1049
        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,
            }
1050
          : undefined
J
JJ Kasper 已提交
1051 1052 1053 1054 1055 1056
      )

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

J
JJ Kasper 已提交
1059 1060 1061 1062
    // If we're here, that means data is missing or it's stale.

    const doRender = withCoalescedInvoke(async function(): Promise<{
      html: string | null
1063
      pageData: any
J
JJ Kasper 已提交
1064 1065
      sprRevalidate: number | false
    }> {
1066
      let pageData: any
J
JJ Kasper 已提交
1067 1068 1069 1070 1071 1072
      let html: string | null
      let sprRevalidate: number | false

      let renderResult
      // handle serverless
      if (isLikeServerless) {
1073
        renderResult = await (components.Component as any).renderReqToHTML(
1074 1075
          req,
          res,
1076
          'passthrough'
1077
        )
J
JJ Kasper 已提交
1078 1079

        html = renderResult.html
1080
        pageData = renderResult.renderOpts.pageData
J
JJ Kasper 已提交
1081 1082
        sprRevalidate = renderResult.renderOpts.revalidate
      } else {
1083
        const renderOpts: RenderOpts = {
1084
          ...components,
J
JJ Kasper 已提交
1085 1086 1087 1088 1089
          ...opts,
        }
        renderResult = await renderToHTML(req, res, pathname, query, renderOpts)

        html = renderResult
1090 1091 1092
        // TODO: change this to a different passing mechanism
        pageData = (renderOpts as any).pageData
        sprRevalidate = (renderOpts as any).revalidate
J
JJ Kasper 已提交
1093 1094
      }

1095
      return { html, pageData, sprRevalidate }
1096
    })
J
JJ Kasper 已提交
1097

1098
    const isProduction = !this.renderOpts.dev
J
Joe Haddad 已提交
1099
    const isDynamicPathname = isDynamicRoute(pathname)
1100
    const didRespond = isResSent(res)
1101

1102 1103 1104
    const { staticPaths, hasStaticFallback } = hasStaticPaths
      ? await this.getStaticPaths(pathname)
      : { staticPaths: undefined, hasStaticFallback: false }
1105

1106 1107 1108 1109 1110 1111 1112 1113 1114 1115
    // 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.
    //
1116
    // * Non-dynamic pages should block (though this is an impossible
1117 1118
    //   case in production).
    //
1119 1120
    // * Dynamic pages should return their skeleton if not defined in
    //   getStaticPaths, then finish the data request on the client-side.
1121
    //
J
Joe Haddad 已提交
1122
    if (
1123
      !didRespond &&
J
Joe Haddad 已提交
1124
      !isDataReq &&
1125 1126
      !isPreviewMode &&
      isDynamicPathname &&
1127 1128 1129
      // Development should trigger fallback when the path is not in
      // `getStaticPaths`
      (isProduction || !staticPaths || !staticPaths.includes(urlPathname))
J
Joe Haddad 已提交
1130
    ) {
1131 1132 1133 1134 1135 1136 1137
      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
      ) {
1138
        throw new NoFallbackError()
1139 1140
      }

1141
      let html: string
1142

1143 1144
      // Production already emitted the fallback as static HTML.
      if (isProduction) {
1145
        html = await getFallback(pathname)
1146 1147 1148
      }
      // We need to generate the fallback on-demand for development.
      else {
1149 1150
        query.__nextFallback = 'true'
        if (isLikeServerless) {
1151
          prepareServerlessUrl(req, query)
1152 1153 1154 1155 1156 1157
          const renderResult = await (components.Component as any).renderReqToHTML(
            req,
            res,
            'passthrough'
          )
          html = renderResult.html
1158 1159
        } else {
          html = (await renderToHTML(req, res, pathname, query, {
1160
            ...components,
1161 1162 1163 1164 1165
            ...opts,
          })) as string
        }
      }

1166
      sendPayload(res, html, 'html')
1167 1168
    }

1169 1170 1171 1172 1173
    const {
      isOrigin,
      value: { html, pageData, sprRevalidate },
    } = await doRender(ssgCacheKey, [])
    if (!isResSent(res)) {
1174
      sendPayload(
1175 1176
        res,
        isDataReq ? JSON.stringify(pageData) : html,
1177
        isDataReq ? 'json' : 'html',
1178
        !this.renderOpts.dev
1179 1180 1181 1182 1183
          ? {
              private: isPreviewMode,
              stateful: false, // GSP response
              revalidate: sprRevalidate,
            }
1184
          : undefined
1185 1186
      )
    }
J
JJ Kasper 已提交
1187

1188 1189 1190 1191 1192
    // Update the SPR cache if the head request
    if (isOrigin) {
      // Preview mode should not be stored in cache
      if (!isPreviewMode) {
        await setSprCache(ssgCacheKey, { html: html!, pageData }, sprRevalidate)
J
JJ Kasper 已提交
1193
      }
1194 1195 1196
    }

    return null
1197 1198
  }

1199
  public async renderToHTML(
J
Joe Haddad 已提交
1200 1201 1202
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1203
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1204
  ): Promise<string | null> {
1205 1206 1207
    try {
      const result = await this.findPageComponents(pathname, query)
      if (result) {
1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219
        try {
          return await this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
            result,
            { ...this.renderOpts }
          )
        } catch (err) {
          if (!(err instanceof NoFallbackError)) {
            throw err
          }
1220
        }
1221
      }
J
Joe Haddad 已提交
1222

1223 1224 1225 1226 1227 1228
      if (this.dynamicRoutes) {
        for (const dynamicRoute of this.dynamicRoutes) {
          const params = dynamicRoute.match(pathname)
          if (!params) {
            continue
          }
J
Joe Haddad 已提交
1229

1230 1231 1232 1233 1234 1235
          const result = await this.findPageComponents(
            dynamicRoute.page,
            query,
            params
          )
          if (result) {
1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247
            try {
              return await this.renderToHTMLWithComponents(
                req,
                res,
                dynamicRoute.page,
                result,
                { ...this.renderOpts, params }
              )
            } catch (err) {
              if (!(err instanceof NoFallbackError)) {
                throw err
              }
1248
            }
J
Joe Haddad 已提交
1249 1250
          }
        }
1251 1252 1253 1254 1255 1256 1257 1258 1259
      }
    } 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 已提交
1260 1261
  }

J
Joe Haddad 已提交
1262 1263 1264 1265 1266
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1267
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1268 1269 1270
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
1271
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
1272
    )
N
Naoyuki Kanezawa 已提交
1273
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
1274
    if (html === null) {
1275 1276
      return
    }
1277
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
1278 1279
  }

1280 1281 1282 1283 1284 1285 1286 1287 1288
  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 已提交
1289 1290 1291 1292 1293
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
1294
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1295
  ) {
1296
    let result: null | FindComponentsResult = null
1297

1298 1299 1300
    const is404 = res.statusCode === 404
    let using404Page = false

1301
    // use static 404 page if available and is 404 response
1302
    if (is404) {
1303 1304
      result = await this.findPageComponents('/404')
      using404Page = result !== null
1305 1306 1307 1308 1309 1310
    }

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

1311 1312 1313 1314 1315 1316 1317 1318
    if (
      process.env.NODE_ENV !== 'production' &&
      !using404Page &&
      (await this.hasPage('/_error'))
    ) {
      this.customErrorNo404Warn()
    }

1319
    let html: string | null
1320
    try {
1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334
      try {
        html = await this.renderToHTMLWithComponents(
          req,
          res,
          using404Page ? '/404' : '/_error',
          result!,
          {
            ...this.renderOpts,
            err,
          }
        )
      } catch (err) {
        if (err instanceof NoFallbackError) {
          throw new Error('invariant: failed to render error page')
1335
        }
1336
        throw err
1337
      }
1338 1339 1340 1341 1342 1343
    } catch (err) {
      console.error(err)
      res.statusCode = 500
      html = 'Internal Server Error'
    }
    return html
N
Naoyuki Kanezawa 已提交
1344 1345
  }

J
Joe Haddad 已提交
1346 1347 1348
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
1349
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1350
  ): Promise<void> {
1351 1352
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
N
Naoyuki Kanezawa 已提交
1353
    res.statusCode = 404
1354
    return this.renderError(null, req, res, pathname!, query)
N
Naoyuki Kanezawa 已提交
1355
  }
N
Naoyuki Kanezawa 已提交
1356

J
Joe Haddad 已提交
1357 1358 1359 1360
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
1361
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1362
  ): Promise<void> {
A
Arunoda Susiripala 已提交
1363
    if (!this.isServeableUrl(path)) {
1364
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
1365 1366
    }

1367 1368 1369 1370 1371 1372
    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 已提交
1373
    try {
1374
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
1375
    } catch (err) {
T
Tim Neutkens 已提交
1376
      if (err.code === 'ENOENT' || err.statusCode === 404) {
1377
        this.render404(req, res, parsedUrl)
1378 1379 1380
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
1381 1382 1383 1384 1385 1386
      } else {
        throw err
      }
    }
  }

1387 1388 1389 1390 1391 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 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445
  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)) {
      userFilesStatic = recursiveReadDirSync(pathUserFilesStatic).map(f =>
        join('.', 'static', f)
      )
    }

    let userFilesPublic: string[] = []
    if (this.publicDir && fs.existsSync(this.publicDir)) {
      userFilesPublic = recursiveReadDirSync(this.publicDir).map(f =>
        join('.', 'public', f)
      )
    }

    let nextFilesStatic: string[] = []
    nextFilesStatic = recursiveReadDirSync(
      join(this.distDir, 'static')
    ).map(f => join('.', relative(this.dir, this.distDir), 'static', f))

    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 已提交
1446
    if (
1447 1448 1449
      (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 已提交
1450 1451 1452 1453
    ) {
      return false
    }

1454 1455 1456 1457
    // Check against the real filesystem paths
    const filesystemUrls = this.getFilesystemPaths()
    const resolved = relative(this.dir, untrustedFilePath)
    return filesystemUrls.has(resolved)
A
Arunoda Susiripala 已提交
1458 1459
  }

1460
  protected readBuildId(): string {
1461 1462 1463 1464 1465
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
1466
        throw new Error(
1467
          `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 已提交
1468
        )
1469 1470 1471
      }

      throw err
1472
    }
1473
  }
1474 1475 1476 1477

  private get _isLikeServerless(): boolean {
    return isTargetLikeServerless(this.nextConfig.target)
  }
1478
}
1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490

function prepareServerlessUrl(req: IncomingMessage, query: ParsedUrlQuery) {
  const curUrl = parseUrl(req.url!, true)
  req.url = formatUrl({
    ...curUrl,
    search: undefined,
    query: {
      ...curUrl.query,
      ...query,
    },
  })
}
1491 1492

class NoFallbackError extends Error {}