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

const getCustomRouteMatcher = pathMatch(true)
65 66 67

type NextConfig = any

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

74 75 76 77 78
type FindComponentsResult = {
  components: LoadComponentsReturnType
  query: ParsedUrlQuery
}

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

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

J
Joe Haddad 已提交
132 133 134 135 136
  public constructor({
    dir = '.',
    staticMarkup = false,
    quiet = false,
    conf = null,
J
JJ Kasper 已提交
137
    dev = false,
J
Joe Haddad 已提交
138
  }: ServerConstructor = {}) {
N
nkzawa 已提交
139
    this.dir = resolve(dir)
N
Naoyuki Kanezawa 已提交
140
    this.quiet = quiet
T
Tim Neutkens 已提交
141
    const phase = this.currentPhase()
142
    this.nextConfig = loadConfig(phase, this.dir, conf)
143
    this.distDir = join(this.dir, this.nextConfig.distDir)
144
    this.publicDir = join(this.dir, CLIENT_PUBLIC_FILES_PATH)
145
    this.hasStaticDir = fs.existsSync(join(this.dir, 'static'))
T
Tim Neutkens 已提交
146

147 148
    // Only serverRuntimeConfig needs the default
    // publicRuntimeConfig gets it's default in client/index.js
J
Joe Haddad 已提交
149 150 151 152 153
    const {
      serverRuntimeConfig = {},
      publicRuntimeConfig,
      assetPrefix,
      generateEtags,
154
      compress,
J
Joe Haddad 已提交
155
    } = this.nextConfig
156

T
Tim Neutkens 已提交
157
    this.buildId = this.readBuildId()
158

159
    this.renderOpts = {
T
Tim Neutkens 已提交
160
      poweredByHeader: this.nextConfig.poweredByHeader,
161
      canonicalBase: this.nextConfig.amp.canonicalBase,
162 163
      documentMiddlewareEnabled: this.nextConfig.experimental
        .documentMiddleware,
J
Joe Haddad 已提交
164
      hasCssMode: this.nextConfig.experimental.css,
165
      staticMarkup,
166
      buildId: this.buildId,
167
      generateEtags,
168
    }
N
Naoyuki Kanezawa 已提交
169

170 171
    // Only the `publicRuntimeConfig` key is exposed to the client side
    // It'll be rendered as part of __NEXT_DATA__ on the client side
172
    if (Object.keys(publicRuntimeConfig).length > 0) {
173
      this.renderOpts.runtimeConfig = publicRuntimeConfig
174 175
    }

176
    if (compress && this.nextConfig.target === 'server') {
177 178 179
      this.compression = compression() as Middleware
    }

180
    // Initialize next/config with the environment configuration
181 182 183 184
    envConfig.setConfig({
      serverRuntimeConfig,
      publicRuntimeConfig,
    })
185

186 187 188 189 190 191 192 193 194 195
    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 已提交
196
    this.router = new Router(this.generateRoutes())
197
    this.setAssetPrefix(assetPrefix)
J
JJ Kasper 已提交
198

199 200 201
    // call init-server middleware, this is also handled
    // individually in serverless bundles when deployed
    if (!dev && this.nextConfig.experimental.plugins) {
202 203
      const initServer = require(join(this.serverBuildDir, 'init-server.js'))
        .default
204
      this.onErrorMiddleware = require(join(
205
        this.serverBuildDir,
206 207 208 209 210
        'on-error-server.js'
      )).default
      initServer()
    }

J
JJ Kasper 已提交
211 212 213 214 215 216 217 218 219 220 221 222
    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 已提交
223
  }
N
nkzawa 已提交
224

225
  protected currentPhase(): string {
226
    return PHASE_PRODUCTION_SERVER
227 228
  }

229 230 231 232
  private logError(err: Error): void {
    if (this.onErrorMiddleware) {
      this.onErrorMiddleware({ err })
    }
233 234
    if (this.quiet) return
    // tslint:disable-next-line
235
    console.error(err)
236 237
  }

238
  private async handleRequest(
J
Joe Haddad 已提交
239 240
    req: IncomingMessage,
    res: ServerResponse,
241
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
242
  ): Promise<void> {
243
    // Parse url if parsedUrl not provided
244
    if (!parsedUrl || typeof parsedUrl !== 'object') {
245 246
      const url: any = req.url
      parsedUrl = parseUrl(url, true)
247
    }
248

249 250 251
    // 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 已提交
252
    }
253

T
Tim Neutkens 已提交
254 255 256 257 258 259 260 261 262 263
    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, '')
    }

264
    res.statusCode = 200
265 266 267
    try {
      return await this.run(req, res, parsedUrl)
    } catch (err) {
J
Joe Haddad 已提交
268 269 270
      this.logError(err)
      res.statusCode = 500
      res.end('Internal Server Error')
271
    }
272 273
  }

274
  public getRequestHandler() {
275
    return this.handleRequest.bind(this)
N
nkzawa 已提交
276 277
  }

278
  public setAssetPrefix(prefix?: string) {
279
    this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
280 281
  }

282
  // Backwards compatibility
283
  public async prepare(): Promise<void> {}
N
nkzawa 已提交
284

T
Tim Neutkens 已提交
285
  // Backwards compatibility
286
  protected async close(): Promise<void> {}
T
Tim Neutkens 已提交
287

288
  protected setImmutableAssetCacheControl(res: ServerResponse) {
T
Tim Neutkens 已提交
289
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
N
nkzawa 已提交
290 291
  }

J
JJ Kasper 已提交
292 293 294 295
  protected getCustomRoutes() {
    return require(join(this.distDir, ROUTES_MANIFEST))
  }

296 297 298 299
  private _cachedPreviewManifest: PrerenderManifest | undefined
  protected getPrerenderManifest(): PrerenderManifest {
    if (this._cachedPreviewManifest) {
      return this._cachedPreviewManifest
J
Joe Haddad 已提交
300
    }
301 302 303 304 305 306
    const manifest = require(join(this.distDir, PRERENDER_MANIFEST))
    return (this._cachedPreviewManifest = manifest)
  }

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

309
  protected generateRoutes(): {
310 311
    headers: Route[]
    rewrites: Route[]
312
    fsRoutes: Route[]
313
    redirects: Route[]
314 315
    catchAllRoute: Route
    pageChecker: PageChecker
316
    useFileSystemPublicRoutes: boolean
317 318
    dynamicRoutes: DynamicRoutes | undefined
  } {
J
JJ Kasper 已提交
319 320
    this.customRoutes = this.getCustomRoutes()

321 322 323
    const publicRoutes = fs.existsSync(this.publicDir)
      ? this.generatePublicRoutes()
      : []
J
JJ Kasper 已提交
324

325
    const staticFilesRoute = this.hasStaticDir
326 327 328 329 330 331 332
      ? [
          {
            // 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*'),
333
            name: 'static catchall',
334 335 336
            fn: async (req, res, params, parsedUrl) => {
              const p = join(this.dir, 'static', ...(params.path || []))
              await this.serveStatic(req, res, p, parsedUrl)
337 338 339
              return {
                finished: true,
              }
340 341 342 343
            },
          } as Route,
        ]
      : []
344

345 346 347 348
    let headers: Route[] = []
    let rewrites: Route[] = []
    let redirects: Route[] = []

349
    const fsRoutes: Route[] = [
T
Tim Neutkens 已提交
350
      {
351
        match: route('/_next/static/:path*'),
352 353
        type: 'route',
        name: '_next/static catchall',
354
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
355 356 357
          // 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.
358 359

          // make sure to 404 for /_next/static itself
360 361 362 363 364 365
          if (!params.path) {
            await this.render404(req, res, parsedUrl)
            return {
              finished: true,
            }
          }
366

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

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

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

J
JJ Kasper 已提交
446 447
    if (this.customRoutes) {
      const getCustomRoute = (
448 449
        r: Rewrite | Redirect | Header,
        type: RouteType
450 451 452 453 454 455 456 457
      ) =>
        ({
          ...r,
          type,
          match: getCustomRouteMatcher(r.source),
          name: type,
          fn: async (req, res, params, parsedUrl) => ({ finished: false }),
        } as Route & Rewrite & Header)
J
JJ Kasper 已提交
458

459
      // Headers come very first
460 461 462 463 464 465 466 467 468 469 470 471 472 473
      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 已提交
474

475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496
      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}`)
            }
497

498 499 500 501 502 503 504
            res.end()
            return {
              finished: true,
            }
          },
        } as Route
      })
505

506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525
      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,
526
              })
527
              proxy.web(req, res)
528

529 530 531
              proxy.on('error', (err: Error) => {
                console.error(`Error occurred proxying ${target}`, err)
              })
532
              return {
533
                finished: true,
J
JJ Kasper 已提交
534
              }
535 536
            }
            ;(req as any)._nextDidRewrite = true
537

538 539 540 541 542 543 544 545
            return {
              finished: false,
              pathname: newUrl,
              query: parsedDestination.query,
            }
          },
        } as Route
      })
546 547 548 549 550 551 552 553 554 555 556 557
    }

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

558
        if (params?.path?.[0] === 'api') {
559 560 561
          const handled = await this.handleApiRequest(
            req as NextApiRequest,
            res as NextApiResponse,
562 563
            pathname!,
            query
564 565 566 567 568 569 570
          )
          if (handled) {
            return { finished: true }
          }
        }

        await this.render(req, res, pathname, query, parsedUrl)
571 572 573 574
        return {
          finished: true,
        }
      },
575
    }
576

577
    const { useFileSystemPublicRoutes } = this.nextConfig
J
Joe Haddad 已提交
578

579 580
    if (useFileSystemPublicRoutes) {
      this.dynamicRoutes = this.getDynamicRoutes()
581
    }
N
nkzawa 已提交
582

583
    return {
584
      headers,
585
      fsRoutes,
586 587
      rewrites,
      redirects,
588
      catchAllRoute,
589
      useFileSystemPublicRoutes,
590 591 592
      dynamicRoutes: this.dynamicRoutes,
      pageChecker: this.hasPage.bind(this),
    }
T
Tim Neutkens 已提交
593 594
  }

595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612
  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
  }

613 614 615 616 617 618 619 620 621
  protected async _beforeCatchAllRender(
    _req: IncomingMessage,
    _res: ServerResponse,
    _params: Params,
    _parsedUrl: UrlWithParsedQuery
  ) {
    return false
  }

622 623 624
  // Used to build API page in development
  protected async ensureApiPage(pathname: string) {}

L
Lukáš Huvar 已提交
625 626 627 628 629 630
  /**
   * Resolves `API` request, in development builds on demand
   * @param req http request
   * @param res http response
   * @param pathname path of request
   */
J
Joe Haddad 已提交
631
  private async handleApiRequest(
632 633
    req: IncomingMessage,
    res: ServerResponse,
634 635
    pathname: string,
    query: ParsedUrlQuery
J
Joe Haddad 已提交
636
  ) {
637
    let page = pathname
L
Lukáš Huvar 已提交
638
    let params: Params | boolean = false
639
    let pageFound = await this.hasPage(page)
J
JJ Kasper 已提交
640

641
    if (!pageFound && this.dynamicRoutes) {
L
Lukáš Huvar 已提交
642 643
      for (const dynamicRoute of this.dynamicRoutes) {
        params = dynamicRoute.match(pathname)
644
        if (dynamicRoute.page.startsWith('/api') && params) {
645 646
          page = dynamicRoute.page
          pageFound = true
L
Lukáš Huvar 已提交
647 648 649 650 651
          break
        }
      }
    }

652
    if (!pageFound) {
653
      return false
J
JJ Kasper 已提交
654
    }
655 656 657 658 659 660
    // 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)
661
    query = { ...query, ...params }
J
JJ Kasper 已提交
662

663
    if (!this.renderOpts.dev && this._isLikeServerless) {
664
      if (typeof pageModule.default === 'function') {
665
        prepareServerlessUrl(req, query)
666 667
        await pageModule.default(req, res)
        return true
J
JJ Kasper 已提交
668 669 670
      }
    }

J
Joe Haddad 已提交
671 672 673 674 675 676 677 678 679
    const previewProps = this.getPreviewProps()
    await apiResolver(
      req,
      res,
      query,
      pageModule,
      { ...previewProps },
      this.onErrorMiddleware
    )
680
    return true
L
Lukáš Huvar 已提交
681 682
  }

683
  protected generatePublicRoutes(): Route[] {
684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702
    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
            )
703 704 705
            return {
              finished: true,
            }
706 707 708 709 710 711 712
          }
          return {
            finished: false,
          }
        },
      } as Route,
    ]
713 714
  }

715
  protected getDynamicRoutes() {
716 717 718
    const dynamicRoutedPages = Object.keys(this.pagesManifest!).filter(
      isDynamicRoute
    )
719 720 721 722
    return getSortedRoutes(dynamicRoutedPages).map(page => ({
      page,
      match: getRouteMatcher(getRouteRegex(page)),
    }))
J
Joe Haddad 已提交
723 724
  }

725 726 727 728 729 730
  private handleCompression(req: IncomingMessage, res: ServerResponse) {
    if (this.compression) {
      this.compression(req, res, () => {})
    }
  }

731
  protected async run(
J
Joe Haddad 已提交
732 733
    req: IncomingMessage,
    res: ServerResponse,
734
    parsedUrl: UrlWithParsedQuery
J
Joe Haddad 已提交
735
  ) {
736 737
    this.handleCompression(req, res)

738
    try {
739 740
      const matched = await this.router.execute(req, res, parsedUrl)
      if (matched) {
741 742 743 744 745 746 747 748
        return
      }
    } catch (err) {
      if (err.code === 'DECODE_FAILED') {
        res.statusCode = 400
        return this.renderError(null, req, res, '/_error', {})
      }
      throw err
749 750
    }

751
    await this.render404(req, res, parsedUrl)
N
nkzawa 已提交
752 753
  }

754
  protected async sendHTML(
J
Joe Haddad 已提交
755 756
    req: IncomingMessage,
    res: ServerResponse,
757
    html: string
J
Joe Haddad 已提交
758
  ) {
T
Tim Neutkens 已提交
759 760
    const { generateEtags, poweredByHeader } = this.renderOpts
    return sendHTML(req, res, html, { generateEtags, poweredByHeader })
761 762
  }

J
Joe Haddad 已提交
763 764 765 766 767
  public async render(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
768
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
769
  ): Promise<void> {
770
    const url: any = req.url
771 772 773 774 775

    if (
      url.match(/^\/_next\//) ||
      (this.hasStaticDir && url.match(/^\/static\//))
    ) {
776 777 778
      return this.handleRequest(req, res, parsedUrl)
    }

779
    if (isBlockedPage(pathname)) {
780
      return this.render404(req, res, parsedUrl)
781 782
    }

T
Tim Neutkens 已提交
783
    const html = await this.renderToHTML(req, res, pathname, query, {})
784 785
    // Request was ended by the user
    if (html === null) {
786 787 788
      return
    }

789
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
790
  }
N
nkzawa 已提交
791

J
Joe Haddad 已提交
792
  private async findPageComponents(
J
Joe Haddad 已提交
793
    pathname: string,
794 795 796 797 798 799 800 801 802
    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 已提交
803
      try {
804
        const components = await loadComponents(
J
Joe Haddad 已提交
805 806
          this.distDir,
          this.buildId,
807 808
          pagePath!,
          !this.renderOpts.dev && this._isLikeServerless
J
Joe Haddad 已提交
809
        )
810 811 812
        return {
          components,
          query: {
813
            ...(components.getStaticProps
814 815 816 817 818
              ? { _nextDataReq: query._nextDataReq }
              : query),
            ...(params || {}),
          },
        }
J
JJ Kasper 已提交
819 820 821 822
      } catch (err) {
        if (err.code !== 'ENOENT') throw err
      }
    }
823
    return null
J
Joe Haddad 已提交
824 825
  }

826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866
  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 已提交
867 868 869 870
  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
871
    { components, query }: FindComponentsResult,
872
    opts: any
873
  ): Promise<string | false | null> {
874
    // we need to ensure the status code if /404 is visited directly
875
    if (pathname === '/404') {
876 877 878
      res.statusCode = 404
    }

J
JJ Kasper 已提交
879
    // handle static page
880 881
    if (typeof components.Component === 'string') {
      return components.Component
J
Joe Haddad 已提交
882 883
    }

J
JJ Kasper 已提交
884 885
    // check request state
    const isLikeServerless =
886 887
      typeof components.Component === 'object' &&
      typeof (components.Component as any).renderReqToHTML === 'function'
888 889 890
    const isSSG = !!components.getStaticProps
    const isServerProps = !!components.getServerSideProps
    const hasStaticPaths = !!components.getStaticPaths
891 892 893 894 895 896 897 898 899 900 901 902 903 904 905

    // 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 已提交
906 907

    // non-spr requests should render like normal
908
    if (!isSSG) {
J
JJ Kasper 已提交
909 910
      // handle serverless
      if (isLikeServerless) {
911
        if (isDataReq) {
912
          const renderResult = await (components.Component as any).renderReqToHTML(
913 914 915 916 917
            req,
            res,
            true
          )

918
          sendPayload(
919 920 921
            res,
            JSON.stringify(renderResult?.renderOpts?.pageData),
            'application/json',
922 923 924 925 926 927
            !this.renderOpts.dev
              ? {
                  revalidate: -1,
                  private: false, // Leave to user-land caching
                }
              : undefined
928 929 930
          )
          return null
        }
931
        prepareServerlessUrl(req, query)
932
        return (components.Component as any).renderReqToHTML(req, res)
J
JJ Kasper 已提交
933 934
      }

935 936
      if (isDataReq && isServerProps) {
        const props = await renderToHTML(req, res, pathname, query, {
937
          ...components,
938 939 940
          ...opts,
          isDataReq,
        })
941 942 943 944 945 946 947 948 949 950 951
        sendPayload(
          res,
          JSON.stringify(props),
          'application/json',
          !this.renderOpts.dev
            ? {
                revalidate: -1,
                private: false, // Leave to user-land caching
              }
            : undefined
        )
952 953 954
        return null
      }

J
JJ Kasper 已提交
955
      return renderToHTML(req, res, pathname, query, {
956
        ...components,
J
JJ Kasper 已提交
957 958 959 960
        ...opts,
      })
    }

J
Joe Haddad 已提交
961 962 963 964
    const previewProps = this.getPreviewProps()
    const previewData = tryGetPreviewData(req, res, { ...previewProps })
    const isPreviewMode = previewData !== false

J
JJ Kasper 已提交
965
    // Compute the SPR cache key
966
    const urlPathname = parseUrl(req.url || '').pathname!
J
Joe Haddad 已提交
967 968
    const ssgCacheKey = isPreviewMode
      ? `__` + nanoid() // Preview mode uses a throw away key to not coalesce preview invokes
969
      : urlPathname
J
JJ Kasper 已提交
970 971

    // Complete the response with cached data if its present
J
Joe Haddad 已提交
972 973 974 975
    const cachedData = isPreviewMode
      ? // Preview data bypasses the cache
        undefined
      : await getSprCache(ssgCacheKey)
J
JJ Kasper 已提交
976
    if (cachedData) {
977
      const data = isDataReq
J
JJ Kasper 已提交
978 979 980
        ? JSON.stringify(cachedData.pageData)
        : cachedData.html

981
      sendPayload(
J
JJ Kasper 已提交
982 983
        res,
        data,
984
        isDataReq ? 'application/json' : 'text/html; charset=utf-8',
985
        cachedData.curRevalidate !== undefined && !this.renderOpts.dev
986 987
          ? { revalidate: cachedData.curRevalidate, private: isPreviewMode }
          : undefined
J
JJ Kasper 已提交
988 989 990 991 992 993
      )

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

J
JJ Kasper 已提交
996 997 998 999
    // If we're here, that means data is missing or it's stale.

    const doRender = withCoalescedInvoke(async function(): Promise<{
      html: string | null
1000
      pageData: any
J
JJ Kasper 已提交
1001 1002
      sprRevalidate: number | false
    }> {
1003
      let pageData: any
J
JJ Kasper 已提交
1004 1005 1006 1007 1008 1009
      let html: string | null
      let sprRevalidate: number | false

      let renderResult
      // handle serverless
      if (isLikeServerless) {
1010
        renderResult = await (components.Component as any).renderReqToHTML(
1011 1012 1013 1014
          req,
          res,
          true
        )
J
JJ Kasper 已提交
1015 1016

        html = renderResult.html
1017
        pageData = renderResult.renderOpts.pageData
J
JJ Kasper 已提交
1018 1019 1020
        sprRevalidate = renderResult.renderOpts.revalidate
      } else {
        const renderOpts = {
1021
          ...components,
J
JJ Kasper 已提交
1022 1023 1024 1025 1026
          ...opts,
        }
        renderResult = await renderToHTML(req, res, pathname, query, renderOpts)

        html = renderResult
1027
        pageData = renderOpts.pageData
J
JJ Kasper 已提交
1028 1029 1030
        sprRevalidate = renderOpts.revalidate
      }

1031
      return { html, pageData, sprRevalidate }
1032
    })
J
JJ Kasper 已提交
1033

1034
    const isProduction = !this.renderOpts.dev
J
Joe Haddad 已提交
1035
    const isDynamicPathname = isDynamicRoute(pathname)
1036
    const didRespond = isResSent(res)
1037

1038 1039 1040
    const { staticPaths, hasStaticFallback } = hasStaticPaths
      ? await this.getStaticPaths(pathname)
      : { staticPaths: undefined, hasStaticFallback: false }
1041

1042 1043 1044 1045 1046 1047 1048 1049 1050 1051
    // 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.
    //
1052
    // * Non-dynamic pages should block (though this is an impossible
1053 1054
    //   case in production).
    //
1055 1056
    // * Dynamic pages should return their skeleton if not defined in
    //   getStaticPaths, then finish the data request on the client-side.
1057
    //
J
Joe Haddad 已提交
1058
    if (
1059
      !didRespond &&
J
Joe Haddad 已提交
1060
      !isDataReq &&
1061 1062
      !isPreviewMode &&
      isDynamicPathname &&
1063 1064 1065
      // Development should trigger fallback when the path is not in
      // `getStaticPaths`
      (isProduction || !staticPaths || !staticPaths.includes(urlPathname))
J
Joe Haddad 已提交
1066
    ) {
1067 1068 1069 1070 1071 1072 1073 1074 1075 1076
      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
      ) {
        return false
      }

1077
      let html: string
1078

1079 1080
      // Production already emitted the fallback as static HTML.
      if (isProduction) {
1081
        html = await getFallback(pathname)
1082 1083 1084
      }
      // We need to generate the fallback on-demand for development.
      else {
1085 1086
        query.__nextFallback = 'true'
        if (isLikeServerless) {
1087
          prepareServerlessUrl(req, query)
1088
          html = await (components.Component as any).renderReqToHTML(req, res)
1089 1090
        } else {
          html = (await renderToHTML(req, res, pathname, query, {
1091
            ...components,
1092 1093 1094 1095 1096
            ...opts,
          })) as string
        }
      }

1097
      sendPayload(res, html, 'text/html; charset=utf-8')
1098 1099
    }

1100 1101 1102 1103 1104
    const {
      isOrigin,
      value: { html, pageData, sprRevalidate },
    } = await doRender(ssgCacheKey, [])
    if (!isResSent(res)) {
1105
      sendPayload(
1106 1107 1108
        res,
        isDataReq ? JSON.stringify(pageData) : html,
        isDataReq ? 'application/json' : 'text/html; charset=utf-8',
1109 1110 1111
        !this.renderOpts.dev
          ? { revalidate: sprRevalidate, private: isPreviewMode }
          : undefined
1112 1113
      )
    }
J
JJ Kasper 已提交
1114

1115 1116 1117 1118 1119
    // 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 已提交
1120
      }
1121 1122 1123
    }

    return null
1124 1125
  }

1126
  public async renderToHTML(
J
Joe Haddad 已提交
1127 1128 1129 1130
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
J
Joe Haddad 已提交
1131 1132 1133 1134 1135 1136
    {
      amphtml,
      hasAmp,
    }: {
      amphtml?: boolean
      hasAmp?: boolean
1137
    } = {}
J
Joe Haddad 已提交
1138
  ): Promise<string | null> {
1139 1140 1141
    try {
      const result = await this.findPageComponents(pathname, query)
      if (result) {
1142
        const result2 = await this.renderToHTMLWithComponents(
1143 1144 1145 1146 1147 1148
          req,
          res,
          pathname,
          result,
          { ...this.renderOpts, amphtml, hasAmp }
        )
1149 1150 1151
        if (result2 !== false) {
          return result2
        }
1152
      }
J
Joe Haddad 已提交
1153

1154 1155 1156 1157 1158 1159
      if (this.dynamicRoutes) {
        for (const dynamicRoute of this.dynamicRoutes) {
          const params = dynamicRoute.match(pathname)
          if (!params) {
            continue
          }
J
Joe Haddad 已提交
1160

1161 1162 1163 1164 1165 1166
          const result = await this.findPageComponents(
            dynamicRoute.page,
            query,
            params
          )
          if (result) {
1167
            const result2 = await this.renderToHTMLWithComponents(
1168 1169 1170 1171 1172 1173 1174 1175 1176
              req,
              res,
              dynamicRoute.page,
              result,
              {
                ...this.renderOpts,
                params,
                amphtml,
                hasAmp,
1177
              }
J
Joe Haddad 已提交
1178
            )
1179 1180 1181
            if (result2 !== false) {
              return result2
            }
J
Joe Haddad 已提交
1182 1183
          }
        }
1184 1185 1186 1187 1188 1189 1190 1191 1192
      }
    } 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 已提交
1193 1194
  }

J
Joe Haddad 已提交
1195 1196 1197 1198 1199
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1200
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1201 1202 1203
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
1204
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
1205
    )
N
Naoyuki Kanezawa 已提交
1206
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
1207
    if (html === null) {
1208 1209
      return
    }
1210
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
1211 1212
  }

J
Joe Haddad 已提交
1213 1214 1215 1216 1217
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
1218
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1219
  ) {
1220
    let result: null | FindComponentsResult = null
1221

1222 1223 1224
    const is404 = res.statusCode === 404
    let using404Page = false

1225
    // use static 404 page if available and is 404 response
1226
    if (is404) {
1227 1228
      result = await this.findPageComponents('/404')
      using404Page = result !== null
1229 1230 1231 1232 1233 1234
    }

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

1235
    let html: string | null
1236
    try {
1237
      const result2 = await this.renderToHTMLWithComponents(
1238 1239
        req,
        res,
1240
        using404Page ? '/404' : '/_error',
1241
        result!,
1242 1243 1244 1245 1246
        {
          ...this.renderOpts,
          err,
        }
      )
1247 1248 1249 1250
      if (result2 === false) {
        throw new Error('invariant: failed to render error page')
      }
      html = result2
1251 1252 1253 1254 1255 1256
    } catch (err) {
      console.error(err)
      res.statusCode = 500
      html = 'Internal Server Error'
    }
    return html
N
Naoyuki Kanezawa 已提交
1257 1258
  }

J
Joe Haddad 已提交
1259 1260 1261
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
1262
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1263
  ): Promise<void> {
1264 1265
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
N
Naoyuki Kanezawa 已提交
1266
    res.statusCode = 404
1267
    return this.renderError(null, req, res, pathname!, query)
N
Naoyuki Kanezawa 已提交
1268
  }
N
Naoyuki Kanezawa 已提交
1269

J
Joe Haddad 已提交
1270 1271 1272 1273
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
1274
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1275
  ): Promise<void> {
A
Arunoda Susiripala 已提交
1276
    if (!this.isServeableUrl(path)) {
1277
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
1278 1279
    }

1280 1281 1282 1283 1284 1285
    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 已提交
1286
    try {
1287
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
1288
    } catch (err) {
T
Tim Neutkens 已提交
1289
      if (err.code === 'ENOENT' || err.statusCode === 404) {
1290
        this.render404(req, res, parsedUrl)
1291 1292 1293
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
1294 1295 1296 1297 1298 1299
      } else {
        throw err
      }
    }
  }

1300
  private isServeableUrl(path: string): boolean {
A
Arunoda Susiripala 已提交
1301 1302
    const resolved = resolve(path)
    if (
1303
      resolved.indexOf(join(this.distDir) + sep) !== 0 &&
1304 1305
      resolved.indexOf(join(this.dir, 'static') + sep) !== 0 &&
      resolved.indexOf(join(this.dir, 'public') + sep) !== 0
A
Arunoda Susiripala 已提交
1306 1307 1308 1309 1310 1311 1312 1313
    ) {
      // Seems like the user is trying to traverse the filesystem.
      return false
    }

    return true
  }

1314
  protected readBuildId(): string {
1315 1316 1317 1318 1319
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
1320
        throw new Error(
1321
          `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 已提交
1322
        )
1323 1324 1325
      }

      throw err
1326
    }
1327
  }
1328 1329 1330 1331

  private get _isLikeServerless(): boolean {
    return isTargetLikeServerless(this.nextConfig.target)
  }
1332
}
1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376

function sendPayload(
  res: ServerResponse,
  payload: any,
  type: string,
  options?: { revalidate: number | false; private: boolean }
) {
  // TODO: ETag? Cache-Control headers? Next-specific headers?
  res.setHeader('Content-Type', type)
  res.setHeader('Content-Length', Buffer.byteLength(payload))
  if (options != null) {
    if (options?.private) {
      res.setHeader(
        'Cache-Control',
        `private, no-cache, no-store, max-age=0, must-revalidate`
      )
    } else if (options?.revalidate) {
      res.setHeader(
        'Cache-Control',
        options.revalidate < 0
          ? `no-cache, no-store, must-revalidate`
          : `s-maxage=${options.revalidate}, stale-while-revalidate`
      )
    } else if (options?.revalidate === false) {
      res.setHeader(
        'Cache-Control',
        `s-maxage=31536000, stale-while-revalidate`
      )
    }
  }
  res.end(payload)
}

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