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

const getCustomRouteMatcher = pathMatch(true)
64 65 66

type NextConfig = any

67 68 69 70 71 72
type Middleware = (
  req: IncomingMessage,
  res: ServerResponse,
  next: (err?: Error) => void
) => void

T
Tim Neutkens 已提交
73
export type ServerConstructor = {
74 75 76
  /**
   * Where the Next project is located - @default '.'
   */
J
Joe Haddad 已提交
77 78
  dir?: string
  staticMarkup?: boolean
79 80 81
  /**
   * Hide error messages containing server information - @default false
   */
J
Joe Haddad 已提交
82
  quiet?: boolean
83 84 85
  /**
   * Object what you would use in next.config.js - @default {}
   */
86
  conf?: NextConfig
J
JJ Kasper 已提交
87
  dev?: boolean
88
}
89

N
nkzawa 已提交
90
export default class Server {
91 92 93 94
  dir: string
  quiet: boolean
  nextConfig: NextConfig
  distDir: string
95
  pagesDir?: string
96
  publicDir: string
97
  hasStaticDir: boolean
98 99
  serverBuildDir: string
  pagesManifest?: { [name: string]: string }
100 101
  buildId: string
  renderOpts: {
T
Tim Neutkens 已提交
102
    poweredByHeader: boolean
J
Joe Haddad 已提交
103 104 105 106
    staticMarkup: boolean
    buildId: string
    generateEtags: boolean
    runtimeConfig?: { [key: string]: any }
107 108
    assetPrefix?: string
    canonicalBase: string
109
    documentMiddlewareEnabled: boolean
J
Joe Haddad 已提交
110
    hasCssMode: boolean
111
    dev?: boolean
112
    pages404?: boolean
113
  }
114
  private compression?: Middleware
J
JJ Kasper 已提交
115
  private onErrorMiddleware?: ({ err }: { err: Error }) => Promise<void>
116
  router: Router
117
  protected dynamicRoutes?: DynamicRoutes
J
JJ Kasper 已提交
118 119 120
  protected customRoutes?: {
    rewrites: Rewrite[]
    redirects: Redirect[]
121
    headers: Header[]
J
JJ Kasper 已提交
122
  }
123

J
Joe Haddad 已提交
124 125 126 127 128
  public constructor({
    dir = '.',
    staticMarkup = false,
    quiet = false,
    conf = null,
J
JJ Kasper 已提交
129
    dev = false,
J
Joe Haddad 已提交
130
  }: ServerConstructor = {}) {
N
nkzawa 已提交
131
    this.dir = resolve(dir)
N
Naoyuki Kanezawa 已提交
132
    this.quiet = quiet
T
Tim Neutkens 已提交
133
    const phase = this.currentPhase()
134
    this.nextConfig = loadConfig(phase, this.dir, conf)
135
    this.distDir = join(this.dir, this.nextConfig.distDir)
136
    this.publicDir = join(this.dir, CLIENT_PUBLIC_FILES_PATH)
137
    this.hasStaticDir = fs.existsSync(join(this.dir, 'static'))
T
Tim Neutkens 已提交
138

139 140
    // Only serverRuntimeConfig needs the default
    // publicRuntimeConfig gets it's default in client/index.js
J
Joe Haddad 已提交
141 142 143 144 145
    const {
      serverRuntimeConfig = {},
      publicRuntimeConfig,
      assetPrefix,
      generateEtags,
146
      compress,
J
Joe Haddad 已提交
147
    } = this.nextConfig
148

T
Tim Neutkens 已提交
149
    this.buildId = this.readBuildId()
150

151
    this.renderOpts = {
T
Tim Neutkens 已提交
152
      poweredByHeader: this.nextConfig.poweredByHeader,
153
      canonicalBase: this.nextConfig.amp.canonicalBase,
154 155
      documentMiddlewareEnabled: this.nextConfig.experimental
        .documentMiddleware,
J
Joe Haddad 已提交
156
      hasCssMode: this.nextConfig.experimental.css,
157
      staticMarkup,
158
      buildId: this.buildId,
159
      generateEtags,
160
      pages404: this.nextConfig.experimental.pages404,
161
    }
N
Naoyuki Kanezawa 已提交
162

163 164
    // Only the `publicRuntimeConfig` key is exposed to the client side
    // It'll be rendered as part of __NEXT_DATA__ on the client side
165
    if (Object.keys(publicRuntimeConfig).length > 0) {
166
      this.renderOpts.runtimeConfig = publicRuntimeConfig
167 168
    }

169
    if (compress && this.nextConfig.target === 'server') {
170 171 172
      this.compression = compression() as Middleware
    }

173
    // Initialize next/config with the environment configuration
174 175 176 177
    envConfig.setConfig({
      serverRuntimeConfig,
      publicRuntimeConfig,
    })
178

179 180 181 182 183 184 185 186 187 188
    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 已提交
189
    this.router = new Router(this.generateRoutes())
190
    this.setAssetPrefix(assetPrefix)
J
JJ Kasper 已提交
191

192 193 194
    // call init-server middleware, this is also handled
    // individually in serverless bundles when deployed
    if (!dev && this.nextConfig.experimental.plugins) {
195 196
      const initServer = require(join(this.serverBuildDir, 'init-server.js'))
        .default
197
      this.onErrorMiddleware = require(join(
198
        this.serverBuildDir,
199 200 201 202 203
        'on-error-server.js'
      )).default
      initServer()
    }

J
JJ Kasper 已提交
204 205 206 207 208 209 210 211 212 213 214 215
    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 已提交
216
  }
N
nkzawa 已提交
217

218
  protected currentPhase(): string {
219
    return PHASE_PRODUCTION_SERVER
220 221
  }

222 223 224 225
  private logError(err: Error): void {
    if (this.onErrorMiddleware) {
      this.onErrorMiddleware({ err })
    }
226 227
    if (this.quiet) return
    // tslint:disable-next-line
228
    console.error(err)
229 230
  }

J
Joe Haddad 已提交
231 232 233
  private handleRequest(
    req: IncomingMessage,
    res: ServerResponse,
234
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
235
  ): Promise<void> {
236
    // Parse url if parsedUrl not provided
237
    if (!parsedUrl || typeof parsedUrl !== 'object') {
238 239
      const url: any = req.url
      parsedUrl = parseUrl(url, true)
240
    }
241

242 243 244
    // 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 已提交
245
    }
246

T
Tim Neutkens 已提交
247 248 249 250 251 252 253 254 255 256
    if (parsedUrl.pathname!.startsWith(this.nextConfig.experimental.basePath)) {
      // If replace ends up replacing the full url it'll be `undefined`, meaning we have to default it to `/`
      parsedUrl.pathname =
        parsedUrl.pathname!.replace(
          this.nextConfig.experimental.basePath,
          ''
        ) || '/'
      req.url = req.url!.replace(this.nextConfig.experimental.basePath, '')
    }

257
    res.statusCode = 200
258
    return this.run(req, res, parsedUrl).catch(err => {
J
Joe Haddad 已提交
259 260 261 262
      this.logError(err)
      res.statusCode = 500
      res.end('Internal Server Error')
    })
263 264
  }

265
  public getRequestHandler() {
266
    return this.handleRequest.bind(this)
N
nkzawa 已提交
267 268
  }

269
  public setAssetPrefix(prefix?: string) {
270
    this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
271 272
  }

273
  // Backwards compatibility
274
  public async prepare(): Promise<void> {}
N
nkzawa 已提交
275

T
Tim Neutkens 已提交
276
  // Backwards compatibility
277
  protected async close(): Promise<void> {}
T
Tim Neutkens 已提交
278

279
  protected setImmutableAssetCacheControl(res: ServerResponse) {
T
Tim Neutkens 已提交
280
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
N
nkzawa 已提交
281 282
  }

J
JJ Kasper 已提交
283 284 285 286
  protected getCustomRoutes() {
    return require(join(this.distDir, ROUTES_MANIFEST))
  }

J
Joe Haddad 已提交
287 288 289 290 291 292 293 294 295 296 297
  private _cachedPreviewProps: __ApiPreviewProps | undefined
  protected getPreviewProps(): __ApiPreviewProps {
    if (this._cachedPreviewProps) {
      return this._cachedPreviewProps
    }
    return (this._cachedPreviewProps = require(join(
      this.distDir,
      PRERENDER_MANIFEST
    )).preview)
  }

298
  protected generateRoutes(): {
299 300
    headers: Route[]
    rewrites: Route[]
301
    fsRoutes: Route[]
302
    redirects: Route[]
303 304
    catchAllRoute: Route
    pageChecker: PageChecker
305
    useFileSystemPublicRoutes: boolean
306 307
    dynamicRoutes: DynamicRoutes | undefined
  } {
J
JJ Kasper 已提交
308 309
    this.customRoutes = this.getCustomRoutes()

310 311 312
    const publicRoutes = fs.existsSync(this.publicDir)
      ? this.generatePublicRoutes()
      : []
J
JJ Kasper 已提交
313

314
    const staticFilesRoute = this.hasStaticDir
315 316 317 318 319 320 321
      ? [
          {
            // 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*'),
322
            name: 'static catchall',
323 324 325
            fn: async (req, res, params, parsedUrl) => {
              const p = join(this.dir, 'static', ...(params.path || []))
              await this.serveStatic(req, res, p, parsedUrl)
326 327 328
              return {
                finished: true,
              }
329 330 331 332
            },
          } as Route,
        ]
      : []
333

334 335 336 337
    let headers: Route[] = []
    let rewrites: Route[] = []
    let redirects: Route[] = []

338
    const fsRoutes: Route[] = [
T
Tim Neutkens 已提交
339
      {
340
        match: route('/_next/static/:path*'),
341 342
        type: 'route',
        name: '_next/static catchall',
343
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
344 345 346
          // 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.
347 348

          // make sure to 404 for /_next/static itself
349 350 351 352 353 354
          if (!params.path) {
            await this.render404(req, res, parsedUrl)
            return {
              finished: true,
            }
          }
355

J
Joe Haddad 已提交
356 357 358
          if (
            params.path[0] === CLIENT_STATIC_FILES_RUNTIME ||
            params.path[0] === 'chunks' ||
359 360
            params.path[0] === 'css' ||
            params.path[0] === 'media' ||
J
Joe Haddad 已提交
361 362
            params.path[0] === this.buildId
          ) {
T
Tim Neutkens 已提交
363
            this.setImmutableAssetCacheControl(res)
364
          }
J
Joe Haddad 已提交
365 366 367
          const p = join(
            this.distDir,
            CLIENT_STATIC_FILES_PATH,
368
            ...(params.path || [])
J
Joe Haddad 已提交
369
          )
370
          await this.serveStatic(req, res, p, parsedUrl)
371 372 373
          return {
            finished: true,
          }
374
        },
375
      },
J
JJ Kasper 已提交
376 377
      {
        match: route('/_next/data/:path*'),
378 379
        type: 'route',
        name: '_next/data catchall',
J
JJ Kasper 已提交
380
        fn: async (req, res, params, _parsedUrl) => {
J
JJ Kasper 已提交
381 382 383
          // 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) {
384 385 386 387
            await this.render404(req, res, _parsedUrl)
            return {
              finished: true,
            }
J
JJ Kasper 已提交
388 389 390 391 392 393
          }
          // 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')) {
394 395 396 397
            await this.render404(req, res, _parsedUrl)
            return {
              finished: true,
            }
J
JJ Kasper 已提交
398 399 400 401 402 403 404
          }

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

J
JJ Kasper 已提交
405 406 407 408 409 410
          req.url = pathname
          const parsedUrl = parseUrl(pathname, true)
          await this.render(
            req,
            res,
            pathname,
411
            { ..._parsedUrl.query, _nextDataReq: '1' },
J
JJ Kasper 已提交
412 413
            parsedUrl
          )
414 415 416
          return {
            finished: true,
          }
J
JJ Kasper 已提交
417 418
        },
      },
T
Tim Neutkens 已提交
419
      {
420
        match: route('/_next/:path*'),
421 422
        type: 'route',
        name: '_next catchall',
T
Tim Neutkens 已提交
423
        // This path is needed because `render()` does a check for `/_next` and the calls the routing again
424
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
425
          await this.render404(req, res, parsedUrl)
426 427 428
          return {
            finished: true,
          }
L
Lukáš Huvar 已提交
429 430
        },
      },
431 432
      ...publicRoutes,
      ...staticFilesRoute,
T
Tim Neutkens 已提交
433
    ]
434

J
JJ Kasper 已提交
435 436
    if (this.customRoutes) {
      const getCustomRoute = (
437 438
        r: Rewrite | Redirect | Header,
        type: RouteType
439 440 441 442 443 444 445 446
      ) =>
        ({
          ...r,
          type,
          match: getCustomRouteMatcher(r.source),
          name: type,
          fn: async (req, res, params, parsedUrl) => ({ finished: false }),
        } as Route & Rewrite & Header)
J
JJ Kasper 已提交
447

448
      // Headers come very first
449 450 451 452 453 454 455 456 457 458 459 460 461 462
      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 已提交
463

464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485
      redirects = this.customRoutes.redirects.map(redirect => {
        const route = getCustomRoute(redirect, 'redirect')
        return {
          type: route.type,
          match: route.match,
          statusCode: route.statusCode,
          name: `Redirect route`,
          fn: async (_req, res, params, _parsedUrl) => {
            const { parsedDestination } = prepareDestination(
              route.destination,
              params
            )
            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}`)
            }
486

487 488 489 490 491 492 493
            res.end()
            return {
              finished: true,
            }
          },
        } as Route
      })
494

495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514
      rewrites = this.customRoutes.rewrites.map(rewrite => {
        const route = getCustomRoute(rewrite, 'rewrite')
        return {
          check: true,
          type: route.type,
          name: `Rewrite route`,
          match: route.match,
          fn: async (req, res, params, _parsedUrl) => {
            const { newUrl, parsedDestination } = prepareDestination(
              route.destination,
              params
            )

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

518 519 520
              proxy.on('error', (err: Error) => {
                console.error(`Error occurred proxying ${target}`, err)
              })
521
              return {
522
                finished: true,
J
JJ Kasper 已提交
523
              }
524 525
            }
            ;(req as any)._nextDidRewrite = true
526

527 528 529 530 531 532 533 534
            return {
              finished: false,
              pathname: newUrl,
              query: parsedDestination.query,
            }
          },
        } as Route
      })
535 536 537 538 539 540 541 542 543 544 545 546
    }

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

547
        if (params?.path?.[0] === 'api') {
548 549 550
          const handled = await this.handleApiRequest(
            req as NextApiRequest,
            res as NextApiResponse,
551 552
            pathname!,
            query
553 554 555 556 557 558 559
          )
          if (handled) {
            return { finished: true }
          }
        }

        await this.render(req, res, pathname, query, parsedUrl)
560 561 562 563
        return {
          finished: true,
        }
      },
564
    }
565

566
    const { useFileSystemPublicRoutes } = this.nextConfig
J
Joe Haddad 已提交
567

568 569
    if (useFileSystemPublicRoutes) {
      this.dynamicRoutes = this.getDynamicRoutes()
570
    }
N
nkzawa 已提交
571

572
    return {
573
      headers,
574
      fsRoutes,
575 576
      rewrites,
      redirects,
577
      catchAllRoute,
578
      useFileSystemPublicRoutes,
579 580 581
      dynamicRoutes: this.dynamicRoutes,
      pageChecker: this.hasPage.bind(this),
    }
T
Tim Neutkens 已提交
582 583
  }

584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601
  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
  }

602 603 604 605 606 607 608 609 610
  protected async _beforeCatchAllRender(
    _req: IncomingMessage,
    _res: ServerResponse,
    _params: Params,
    _parsedUrl: UrlWithParsedQuery
  ) {
    return false
  }

611 612 613
  // Used to build API page in development
  protected async ensureApiPage(pathname: string) {}

L
Lukáš Huvar 已提交
614 615 616 617 618 619
  /**
   * Resolves `API` request, in development builds on demand
   * @param req http request
   * @param res http response
   * @param pathname path of request
   */
J
Joe Haddad 已提交
620
  private async handleApiRequest(
621 622
    req: IncomingMessage,
    res: ServerResponse,
623 624
    pathname: string,
    query: ParsedUrlQuery
J
Joe Haddad 已提交
625
  ) {
626
    let page = pathname
L
Lukáš Huvar 已提交
627
    let params: Params | boolean = false
628
    let pageFound = await this.hasPage(page)
J
JJ Kasper 已提交
629

630
    if (!pageFound && this.dynamicRoutes) {
L
Lukáš Huvar 已提交
631 632
      for (const dynamicRoute of this.dynamicRoutes) {
        params = dynamicRoute.match(pathname)
633
        if (dynamicRoute.page.startsWith('/api') && params) {
634 635
          page = dynamicRoute.page
          pageFound = true
L
Lukáš Huvar 已提交
636 637 638 639 640
          break
        }
      }
    }

641
    if (!pageFound) {
642
      return false
J
JJ Kasper 已提交
643
    }
644 645 646 647 648 649
    // 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)
650
    query = { ...query, ...params }
J
JJ Kasper 已提交
651

652
    if (!this.renderOpts.dev && this._isLikeServerless) {
653
      if (typeof pageModule.default === 'function') {
654
        this.prepareServerlessUrl(req, query)
655 656
        await pageModule.default(req, res)
        return true
J
JJ Kasper 已提交
657 658 659
      }
    }

J
Joe Haddad 已提交
660 661 662 663 664 665 666 667 668
    const previewProps = this.getPreviewProps()
    await apiResolver(
      req,
      res,
      query,
      pageModule,
      { ...previewProps },
      this.onErrorMiddleware
    )
669
    return true
L
Lukáš Huvar 已提交
670 671
  }

672
  protected generatePublicRoutes(): Route[] {
673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691
    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) => {
          const path = `/${(params.path || []).join('/')}`

          if (publicFiles.has(path)) {
            await this.serveStatic(
              req,
              res,
              // we need to re-encode it since send decodes it
              join(this.dir, 'public', encodeURIComponent(path)),
              parsedUrl
            )
692 693 694
            return {
              finished: true,
            }
695 696 697 698 699 700 701
          }
          return {
            finished: false,
          }
        },
      } as Route,
    ]
702 703
  }

704
  protected getDynamicRoutes() {
705 706 707
    const dynamicRoutedPages = Object.keys(this.pagesManifest!).filter(
      isDynamicRoute
    )
708 709 710 711
    return getSortedRoutes(dynamicRoutedPages).map(page => ({
      page,
      match: getRouteMatcher(getRouteRegex(page)),
    }))
J
Joe Haddad 已提交
712 713
  }

714 715 716 717 718 719
  private handleCompression(req: IncomingMessage, res: ServerResponse) {
    if (this.compression) {
      this.compression(req, res, () => {})
    }
  }

720
  protected async run(
J
Joe Haddad 已提交
721 722
    req: IncomingMessage,
    res: ServerResponse,
723
    parsedUrl: UrlWithParsedQuery
J
Joe Haddad 已提交
724
  ) {
725 726
    this.handleCompression(req, res)

727
    try {
728 729
      const matched = await this.router.execute(req, res, parsedUrl)
      if (matched) {
730 731 732 733 734 735 736 737
        return
      }
    } catch (err) {
      if (err.code === 'DECODE_FAILED') {
        res.statusCode = 400
        return this.renderError(null, req, res, '/_error', {})
      }
      throw err
738 739
    }

740
    await this.render404(req, res, parsedUrl)
N
nkzawa 已提交
741 742
  }

743
  protected async sendHTML(
J
Joe Haddad 已提交
744 745
    req: IncomingMessage,
    res: ServerResponse,
746
    html: string
J
Joe Haddad 已提交
747
  ) {
T
Tim Neutkens 已提交
748 749
    const { generateEtags, poweredByHeader } = this.renderOpts
    return sendHTML(req, res, html, { generateEtags, poweredByHeader })
750 751
  }

J
Joe Haddad 已提交
752 753 754 755 756
  public async render(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
757
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
758
  ): Promise<void> {
759
    const url: any = req.url
760 761 762 763 764

    if (
      url.match(/^\/_next\//) ||
      (this.hasStaticDir && url.match(/^\/static\//))
    ) {
765 766 767
      return this.handleRequest(req, res, parsedUrl)
    }

768
    if (isBlockedPage(pathname)) {
769
      return this.render404(req, res, parsedUrl)
770 771
    }

T
Tim Neutkens 已提交
772
    const html = await this.renderToHTML(req, res, pathname, query, {})
773 774
    // Request was ended by the user
    if (html === null) {
775 776 777
      return
    }

778
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
779
  }
N
nkzawa 已提交
780

J
Joe Haddad 已提交
781
  private async findPageComponents(
J
Joe Haddad 已提交
782
    pathname: string,
783
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
784
  ) {
785
    const serverless = !this.renderOpts.dev && this._isLikeServerless
J
JJ Kasper 已提交
786 787 788
    // try serving a static AMP version first
    if (query.amp) {
      try {
J
Joe Haddad 已提交
789 790 791
        return await loadComponents(
          this.distDir,
          this.buildId,
792
          normalizePagePath(pathname) + '.amp',
793
          serverless
J
Joe Haddad 已提交
794
        )
J
JJ Kasper 已提交
795 796 797 798
      } catch (err) {
        if (err.code !== 'ENOENT') throw err
      }
    }
J
Joe Haddad 已提交
799 800 801 802
    return await loadComponents(
      this.distDir,
      this.buildId,
      pathname,
803
      serverless
J
Joe Haddad 已提交
804 805 806
    )
  }

J
JJ Kasper 已提交
807 808 809 810 811 812
  private __sendPayload(
    res: ServerResponse,
    payload: any,
    type: string,
    revalidate?: number | false
  ) {
J
JJ Kasper 已提交
813
    // TODO: ETag? Cache-Control headers? Next-specific headers?
J
JJ Kasper 已提交
814
    res.setHeader('Content-Type', type)
J
JJ Kasper 已提交
815
    res.setHeader('Content-Length', Buffer.byteLength(payload))
J
Joe Haddad 已提交
816 817 818 819
    if (!this.renderOpts.dev) {
      if (revalidate) {
        res.setHeader(
          'Cache-Control',
820 821 822
          revalidate < 0
            ? `no-cache, no-store, must-revalidate`
            : `s-maxage=${revalidate}, stale-while-revalidate`
J
Joe Haddad 已提交
823 824 825 826 827 828 829
        )
      } else if (revalidate === false) {
        res.setHeader(
          'Cache-Control',
          `s-maxage=31536000, stale-while-revalidate`
        )
      }
J
JJ Kasper 已提交
830
    }
J
JJ Kasper 已提交
831 832 833
    res.end(payload)
  }

834 835 836 837 838 839 840 841 842 843 844 845
  private prepareServerlessUrl(req: IncomingMessage, query: ParsedUrlQuery) {
    const curUrl = parseUrl(req.url!, true)
    req.url = formatUrl({
      ...curUrl,
      search: undefined,
      query: {
        ...curUrl.query,
        ...query,
      },
    })
  }

J
Joe Haddad 已提交
846 847 848 849 850 851
  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
    result: LoadComponentsReturnType,
852
    opts: any
J
JJ Kasper 已提交
853
  ): Promise<string | null> {
854 855 856 857 858
    // we need to ensure the status code if /404 is visited directly
    if (this.nextConfig.experimental.pages404 && pathname === '/404') {
      res.statusCode = 404
    }

J
JJ Kasper 已提交
859
    // handle static page
J
Joe Haddad 已提交
860 861 862 863
    if (typeof result.Component === 'string') {
      return result.Component
    }

J
JJ Kasper 已提交
864 865
    // check request state
    const isLikeServerless =
J
Joe Haddad 已提交
866
      typeof result.Component === 'object' &&
867
      typeof (result.Component as any).renderReqToHTML === 'function'
868
    const isSSG = !!result.unstable_getStaticProps
869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884
    const isServerProps = !!result.unstable_getServerProps

    // Toggle whether or not this is a Data request
    const isDataReq = query._nextDataReq
    delete query._nextDataReq

    // Serverless requests need its URL transformed back into the original
    // request path (to emulate lambda behavior in production)
    if (isLikeServerless && isDataReq) {
      let { pathname } = parseUrl(req.url || '', true)
      pathname = !pathname || pathname === '/' ? '/index' : pathname
      req.url = formatUrl({
        pathname: `/_next/data/${this.buildId}${pathname}.json`,
        query,
      })
    }
J
JJ Kasper 已提交
885 886

    // non-spr requests should render like normal
887
    if (!isSSG) {
J
JJ Kasper 已提交
888 889
      // handle serverless
      if (isLikeServerless) {
890 891 892 893 894 895 896 897 898 899 900 901 902 903 904
        if (isDataReq) {
          const renderResult = await (result.Component as any).renderReqToHTML(
            req,
            res,
            true
          )

          this.__sendPayload(
            res,
            JSON.stringify(renderResult?.renderOpts?.pageData),
            'application/json',
            -1
          )
          return null
        }
905
        this.prepareServerlessUrl(req, query)
906
        return (result.Component as any).renderReqToHTML(req, res)
J
JJ Kasper 已提交
907 908
      }

909 910 911 912 913 914 915 916 917 918
      if (isDataReq && isServerProps) {
        const props = await renderToHTML(req, res, pathname, query, {
          ...result,
          ...opts,
          isDataReq,
        })
        this.__sendPayload(res, JSON.stringify(props), 'application/json', -1)
        return null
      }

J
JJ Kasper 已提交
919 920 921 922 923 924
      return renderToHTML(req, res, pathname, query, {
        ...result,
        ...opts,
      })
    }

J
Joe Haddad 已提交
925 926 927 928
    const previewProps = this.getPreviewProps()
    const previewData = tryGetPreviewData(req, res, { ...previewProps })
    const isPreviewMode = previewData !== false

J
JJ Kasper 已提交
929
    // Compute the SPR cache key
J
Joe Haddad 已提交
930 931 932
    const ssgCacheKey = isPreviewMode
      ? `__` + nanoid() // Preview mode uses a throw away key to not coalesce preview invokes
      : parseUrl(req.url || '').pathname!
J
JJ Kasper 已提交
933 934

    // Complete the response with cached data if its present
J
Joe Haddad 已提交
935 936 937 938
    const cachedData = isPreviewMode
      ? // Preview data bypasses the cache
        undefined
      : await getSprCache(ssgCacheKey)
J
JJ Kasper 已提交
939
    if (cachedData) {
940
      const data = isDataReq
J
JJ Kasper 已提交
941 942 943 944 945 946
        ? JSON.stringify(cachedData.pageData)
        : cachedData.html

      this.__sendPayload(
        res,
        data,
947
        isDataReq ? 'application/json' : 'text/html; charset=utf-8',
J
JJ Kasper 已提交
948
        cachedData.curRevalidate
J
JJ Kasper 已提交
949 950 951 952 953 954
      )

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

J
JJ Kasper 已提交
957 958 959 960
    // If we're here, that means data is missing or it's stale.

    const doRender = withCoalescedInvoke(async function(): Promise<{
      html: string | null
961
      pageData: any
J
JJ Kasper 已提交
962 963
      sprRevalidate: number | false
    }> {
964
      let pageData: any
J
JJ Kasper 已提交
965 966 967 968 969 970
      let html: string | null
      let sprRevalidate: number | false

      let renderResult
      // handle serverless
      if (isLikeServerless) {
971 972 973 974 975
        renderResult = await (result.Component as any).renderReqToHTML(
          req,
          res,
          true
        )
J
JJ Kasper 已提交
976 977

        html = renderResult.html
978
        pageData = renderResult.renderOpts.pageData
J
JJ Kasper 已提交
979 980 981 982 983 984 985 986 987
        sprRevalidate = renderResult.renderOpts.revalidate
      } else {
        const renderOpts = {
          ...result,
          ...opts,
        }
        renderResult = await renderToHTML(req, res, pathname, query, renderOpts)

        html = renderResult
988
        pageData = renderOpts.pageData
J
JJ Kasper 已提交
989 990 991
        sprRevalidate = renderOpts.revalidate
      }

992
      return { html, pageData, sprRevalidate }
993
    })
J
JJ Kasper 已提交
994

995
    const isProduction = !this.renderOpts.dev
J
Joe Haddad 已提交
996
    const isDynamicPathname = isDynamicRoute(pathname)
997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013
    const didRespond = isResSent(res)
    // 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.
    //
    // * Non-dynamic pages should block (though this is an be an impossible
    //   case in production).
    //
    // * Dynamic pages should return their skeleton, then finish the data
    //   request on the client-side.
    //
J
Joe Haddad 已提交
1014
    if (
1015
      !didRespond &&
J
Joe Haddad 已提交
1016
      !isDataReq &&
1017 1018 1019 1020 1021
      !isPreviewMode &&
      isDynamicPathname &&
      // TODO: development should trigger fallback when the path is not in
      // `getStaticPaths`, for now, let's assume it is.
      isProduction
J
Joe Haddad 已提交
1022
    ) {
1023
      let html: string
1024

1025 1026
      // Production already emitted the fallback as static HTML.
      if (isProduction) {
1027
        html = await getFallback(pathname)
1028 1029 1030
      }
      // We need to generate the fallback on-demand for development.
      else {
1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045
        query.__nextFallback = 'true'
        if (isLikeServerless) {
          this.prepareServerlessUrl(req, query)
          html = await (result.Component as any).renderReqToHTML(req, res)
        } else {
          html = (await renderToHTML(req, res, pathname, query, {
            ...result,
            ...opts,
          })) as string
        }
      }

      this.__sendPayload(res, html, 'text/html; charset=utf-8')
    }

1046 1047
    return doRender(ssgCacheKey, []).then(
      async ({ isOrigin, value: { html, pageData, sprRevalidate } }) => {
J
JJ Kasper 已提交
1048 1049 1050 1051
        // Respond to the request if a payload wasn't sent above (from cache)
        if (!isResSent(res)) {
          this.__sendPayload(
            res,
1052 1053
            isDataReq ? JSON.stringify(pageData) : html,
            isDataReq ? 'application/json' : 'text/html; charset=utf-8',
J
JJ Kasper 已提交
1054
            sprRevalidate
J
JJ Kasper 已提交
1055 1056 1057 1058 1059
          )
        }

        // Update the SPR cache if the head request
        if (isOrigin) {
J
Joe Haddad 已提交
1060 1061 1062 1063 1064 1065 1066 1067
          // Preview mode should not be stored in cache
          if (!isPreviewMode) {
            await setSprCache(
              ssgCacheKey,
              { html: html!, pageData },
              sprRevalidate
            )
          }
J
JJ Kasper 已提交
1068 1069 1070 1071 1072
        }

        return null
      }
    )
1073 1074
  }

J
Joe Haddad 已提交
1075
  public renderToHTML(
J
Joe Haddad 已提交
1076 1077 1078 1079
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
J
Joe Haddad 已提交
1080 1081 1082 1083 1084 1085
    {
      amphtml,
      hasAmp,
    }: {
      amphtml?: boolean
      hasAmp?: boolean
1086
    } = {}
J
Joe Haddad 已提交
1087
  ): Promise<string | null> {
J
Joe Haddad 已提交
1088 1089
    return this.findPageComponents(pathname, query)
      .then(
1090
        result => {
J
Joe Haddad 已提交
1091 1092 1093 1094
          return this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
1095
            result.unstable_getStaticProps
1096
              ? { _nextDataReq: query._nextDataReq }
1097
              : query,
J
Joe Haddad 已提交
1098
            result,
T
Tim Neutkens 已提交
1099
            { ...this.renderOpts, amphtml, hasAmp }
J
Joe Haddad 已提交
1100 1101
          )
        },
1102
        err => {
J
Joe Haddad 已提交
1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113
          if (err.code !== 'ENOENT' || !this.dynamicRoutes) {
            return Promise.reject(err)
          }

          for (const dynamicRoute of this.dynamicRoutes) {
            const params = dynamicRoute.match(pathname)
            if (!params) {
              continue
            }

            return this.findPageComponents(dynamicRoute.page, query).then(
1114 1115
              result => {
                return this.renderToHTMLWithComponents(
J
Joe Haddad 已提交
1116 1117 1118
                  req,
                  res,
                  dynamicRoute.page,
J
JJ Kasper 已提交
1119 1120 1121
                  // only add params for SPR enabled pages
                  {
                    ...(result.unstable_getStaticProps
1122
                      ? { _nextDataReq: query._nextDataReq }
J
JJ Kasper 已提交
1123 1124 1125
                      : query),
                    ...params,
                  },
J
Joe Haddad 已提交
1126
                  result,
J
JJ Kasper 已提交
1127 1128
                  {
                    ...this.renderOpts,
1129
                    params,
J
JJ Kasper 已提交
1130 1131 1132
                    amphtml,
                    hasAmp,
                  }
1133
                )
1134
              }
J
Joe Haddad 已提交
1135 1136 1137 1138
            )
          }

          return Promise.reject(err)
1139
        }
J
Joe Haddad 已提交
1140
      )
1141
      .catch(err => {
J
Joe Haddad 已提交
1142 1143 1144 1145 1146 1147 1148 1149 1150
        if (err && err.code === 'ENOENT') {
          res.statusCode = 404
          return this.renderErrorToHTML(null, req, res, pathname, query)
        } else {
          this.logError(err)
          res.statusCode = 500
          return this.renderErrorToHTML(err, req, res, pathname, query)
        }
      })
N
Naoyuki Kanezawa 已提交
1151 1152
  }

J
Joe Haddad 已提交
1153 1154 1155 1156 1157
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1158
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1159 1160 1161
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
1162
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
1163
    )
N
Naoyuki Kanezawa 已提交
1164
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
1165
    if (html === null) {
1166 1167
      return
    }
1168
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
1169 1170
  }

J
Joe Haddad 已提交
1171 1172 1173 1174 1175
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
1176
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1177
  ) {
1178 1179
    let result: null | LoadComponentsReturnType = null

1180 1181 1182 1183
    const { static404, pages404 } = this.nextConfig.experimental
    const is404 = res.statusCode === 404
    let using404Page = false

1184
    // use static 404 page if available and is 404 response
1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205
    if (is404) {
      if (static404) {
        try {
          result = await this.findPageComponents('/_errors/404')
        } catch (err) {
          if (err.code !== 'ENOENT') {
            throw err
          }
        }
      }

      // use 404 if /_errors/404 isn't available which occurs
      // during development and when _app has getInitialProps
      if (!result && pages404) {
        try {
          result = await this.findPageComponents('/404')
          using404Page = true
        } catch (err) {
          if (err.code !== 'ENOENT') {
            throw err
          }
1206 1207 1208 1209 1210 1211 1212 1213
        }
      }
    }

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

1214 1215 1216 1217 1218
    let html
    try {
      html = await this.renderToHTMLWithComponents(
        req,
        res,
1219
        using404Page ? '/404' : '/_error',
1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232
        query,
        result,
        {
          ...this.renderOpts,
          err,
        }
      )
    } catch (err) {
      console.error(err)
      res.statusCode = 500
      html = 'Internal Server Error'
    }
    return html
N
Naoyuki Kanezawa 已提交
1233 1234
  }

J
Joe Haddad 已提交
1235 1236 1237
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
1238
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1239
  ): Promise<void> {
1240 1241
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
1242
    if (!pathname) {
1243 1244
      throw new Error('pathname is undefined')
    }
N
Naoyuki Kanezawa 已提交
1245
    res.statusCode = 404
1246
    return this.renderError(null, req, res, pathname, query)
N
Naoyuki Kanezawa 已提交
1247
  }
N
Naoyuki Kanezawa 已提交
1248

J
Joe Haddad 已提交
1249 1250 1251 1252
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
1253
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1254
  ): Promise<void> {
A
Arunoda Susiripala 已提交
1255
    if (!this.isServeableUrl(path)) {
1256
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
1257 1258
    }

1259 1260 1261 1262 1263 1264
    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 已提交
1265
    try {
1266
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
1267
    } catch (err) {
T
Tim Neutkens 已提交
1268
      if (err.code === 'ENOENT' || err.statusCode === 404) {
1269
        this.render404(req, res, parsedUrl)
1270 1271 1272
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
1273 1274 1275 1276 1277 1278
      } else {
        throw err
      }
    }
  }

1279
  private isServeableUrl(path: string): boolean {
A
Arunoda Susiripala 已提交
1280 1281
    const resolved = resolve(path)
    if (
1282
      resolved.indexOf(join(this.distDir) + sep) !== 0 &&
1283 1284
      resolved.indexOf(join(this.dir, 'static') + sep) !== 0 &&
      resolved.indexOf(join(this.dir, 'public') + sep) !== 0
A
Arunoda Susiripala 已提交
1285 1286 1287 1288 1289 1290 1291 1292
    ) {
      // Seems like the user is trying to traverse the filesystem.
      return false
    }

    return true
  }

1293
  protected readBuildId(): string {
1294 1295 1296 1297 1298
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
1299
        throw new Error(
1300
          `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 已提交
1301
        )
1302 1303 1304
      }

      throw err
1305
    }
1306
  }
1307 1308 1309 1310

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