next-server.ts 37.4 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
  }

  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
830
    { components, query }: FindComponentsResult,
831
    opts: any
832
  ): Promise<string | false | null> {
833
    // we need to ensure the status code if /404 is visited directly
834
    if (pathname === '/404') {
835 836 837
      res.statusCode = 404
    }

J
JJ Kasper 已提交
838
    // handle static page
839 840
    if (typeof components.Component === 'string') {
      return components.Component
J
Joe Haddad 已提交
841 842
    }

J
JJ Kasper 已提交
843 844
    // check request state
    const isLikeServerless =
845 846
      typeof components.Component === 'object' &&
      typeof (components.Component as any).renderReqToHTML === 'function'
847 848 849
    const isSSG = !!components.getStaticProps
    const isServerProps = !!components.getServerSideProps
    const hasStaticPaths = !!components.getStaticPaths
850 851 852 853 854 855 856 857 858 859 860 861 862 863 864

    // 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 已提交
865 866

    // non-spr requests should render like normal
867
    if (!isSSG) {
J
JJ Kasper 已提交
868 869
      // handle serverless
      if (isLikeServerless) {
870
        if (isDataReq) {
871
          const renderResult = await (components.Component as any).renderReqToHTML(
872 873 874 875 876
            req,
            res,
            true
          )

877
          sendPayload(
878 879 880
            res,
            JSON.stringify(renderResult?.renderOpts?.pageData),
            'application/json',
881 882 883 884 885 886
            !this.renderOpts.dev
              ? {
                  revalidate: -1,
                  private: false, // Leave to user-land caching
                }
              : undefined
887 888 889
          )
          return null
        }
890
        prepareServerlessUrl(req, query)
891
        return (components.Component as any).renderReqToHTML(req, res)
J
JJ Kasper 已提交
892 893
      }

894 895
      if (isDataReq && isServerProps) {
        const props = await renderToHTML(req, res, pathname, query, {
896
          ...components,
897 898 899
          ...opts,
          isDataReq,
        })
900 901 902 903 904 905 906 907 908 909 910
        sendPayload(
          res,
          JSON.stringify(props),
          'application/json',
          !this.renderOpts.dev
            ? {
                revalidate: -1,
                private: false, // Leave to user-land caching
              }
            : undefined
        )
911 912 913
        return null
      }

J
JJ Kasper 已提交
914
      return renderToHTML(req, res, pathname, query, {
915
        ...components,
J
JJ Kasper 已提交
916 917 918 919
        ...opts,
      })
    }

J
Joe Haddad 已提交
920 921 922 923
    const previewProps = this.getPreviewProps()
    const previewData = tryGetPreviewData(req, res, { ...previewProps })
    const isPreviewMode = previewData !== false

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

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

940
      sendPayload(
J
JJ Kasper 已提交
941 942
        res,
        data,
943
        isDataReq ? 'application/json' : 'text/html; charset=utf-8',
944
        cachedData.curRevalidate !== undefined && !this.renderOpts.dev
945 946
          ? { revalidate: cachedData.curRevalidate, private: isPreviewMode }
          : undefined
J
JJ Kasper 已提交
947 948 949 950 951 952
      )

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

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

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

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

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

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

990
      return { html, pageData, sprRevalidate }
991
    })
J
JJ Kasper 已提交
992

993
    const isProduction = !this.renderOpts.dev
J
Joe Haddad 已提交
994
    const isDynamicPathname = isDynamicRoute(pathname)
995
    const didRespond = isResSent(res)
996 997 998 999

    // 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
1000
    let hasStaticFallback = false
1001

1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027
    if (hasStaticPaths) {
      if (isProduction) {
        // `staticPaths` is intentionally set to `undefined` as it should've
        // been caught above 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)
1028 1029 1030
      }
    }

1031 1032 1033 1034 1035 1036 1037 1038 1039 1040
    // 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.
    //
1041
    // * Non-dynamic pages should block (though this is an impossible
1042 1043
    //   case in production).
    //
1044 1045
    // * Dynamic pages should return their skeleton if not defined in
    //   getStaticPaths, then finish the data request on the client-side.
1046
    //
J
Joe Haddad 已提交
1047
    if (
1048
      !didRespond &&
J
Joe Haddad 已提交
1049
      !isDataReq &&
1050 1051
      !isPreviewMode &&
      isDynamicPathname &&
1052 1053 1054
      // Development should trigger fallback when the path is not in
      // `getStaticPaths`
      (isProduction || !staticPaths || !staticPaths.includes(urlPathname))
J
Joe Haddad 已提交
1055
    ) {
1056 1057 1058 1059 1060 1061 1062 1063 1064 1065
      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
      }

1066
      let html: string
1067

1068 1069
      // Production already emitted the fallback as static HTML.
      if (isProduction) {
1070
        html = await getFallback(pathname)
1071 1072 1073
      }
      // We need to generate the fallback on-demand for development.
      else {
1074 1075
        query.__nextFallback = 'true'
        if (isLikeServerless) {
1076
          prepareServerlessUrl(req, query)
1077
          html = await (components.Component as any).renderReqToHTML(req, res)
1078 1079
        } else {
          html = (await renderToHTML(req, res, pathname, query, {
1080
            ...components,
1081 1082 1083 1084 1085
            ...opts,
          })) as string
        }
      }

1086
      sendPayload(res, html, 'text/html; charset=utf-8')
1087 1088
    }

1089 1090 1091 1092 1093
    const {
      isOrigin,
      value: { html, pageData, sprRevalidate },
    } = await doRender(ssgCacheKey, [])
    if (!isResSent(res)) {
1094
      sendPayload(
1095 1096 1097
        res,
        isDataReq ? JSON.stringify(pageData) : html,
        isDataReq ? 'application/json' : 'text/html; charset=utf-8',
1098 1099 1100
        !this.renderOpts.dev
          ? { revalidate: sprRevalidate, private: isPreviewMode }
          : undefined
1101 1102
      )
    }
J
JJ Kasper 已提交
1103

1104 1105 1106 1107 1108
    // 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 已提交
1109
      }
1110 1111 1112
    }

    return null
1113 1114
  }

1115
  public async renderToHTML(
J
Joe Haddad 已提交
1116 1117 1118 1119
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
J
Joe Haddad 已提交
1120 1121 1122 1123 1124 1125
    {
      amphtml,
      hasAmp,
    }: {
      amphtml?: boolean
      hasAmp?: boolean
1126
    } = {}
J
Joe Haddad 已提交
1127
  ): Promise<string | null> {
1128 1129 1130
    try {
      const result = await this.findPageComponents(pathname, query)
      if (result) {
1131
        const result2 = await this.renderToHTMLWithComponents(
1132 1133 1134 1135 1136 1137
          req,
          res,
          pathname,
          result,
          { ...this.renderOpts, amphtml, hasAmp }
        )
1138 1139 1140
        if (result2 !== false) {
          return result2
        }
1141
      }
J
Joe Haddad 已提交
1142

1143 1144 1145 1146 1147 1148
      if (this.dynamicRoutes) {
        for (const dynamicRoute of this.dynamicRoutes) {
          const params = dynamicRoute.match(pathname)
          if (!params) {
            continue
          }
J
Joe Haddad 已提交
1149

1150 1151 1152 1153 1154 1155
          const result = await this.findPageComponents(
            dynamicRoute.page,
            query,
            params
          )
          if (result) {
1156
            const result2 = await this.renderToHTMLWithComponents(
1157 1158 1159 1160 1161 1162 1163 1164 1165
              req,
              res,
              dynamicRoute.page,
              result,
              {
                ...this.renderOpts,
                params,
                amphtml,
                hasAmp,
1166
              }
J
Joe Haddad 已提交
1167
            )
1168 1169 1170
            if (result2 !== false) {
              return result2
            }
J
Joe Haddad 已提交
1171 1172
          }
        }
1173 1174 1175 1176 1177 1178 1179 1180 1181
      }
    } 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 已提交
1182 1183
  }

J
Joe Haddad 已提交
1184 1185 1186 1187 1188
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1189
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1190 1191 1192
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
1193
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
1194
    )
N
Naoyuki Kanezawa 已提交
1195
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
1196
    if (html === null) {
1197 1198
      return
    }
1199
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
1200 1201
  }

J
Joe Haddad 已提交
1202 1203 1204 1205 1206
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
1207
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1208
  ) {
1209
    let result: null | FindComponentsResult = null
1210

1211 1212 1213
    const is404 = res.statusCode === 404
    let using404Page = false

1214
    // use static 404 page if available and is 404 response
1215
    if (is404) {
1216 1217
      result = await this.findPageComponents('/404')
      using404Page = result !== null
1218 1219 1220 1221 1222 1223
    }

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

1224
    let html: string | null
1225
    try {
1226
      const result2 = await this.renderToHTMLWithComponents(
1227 1228
        req,
        res,
1229
        using404Page ? '/404' : '/_error',
1230
        result!,
1231 1232 1233 1234 1235
        {
          ...this.renderOpts,
          err,
        }
      )
1236 1237 1238 1239
      if (result2 === false) {
        throw new Error('invariant: failed to render error page')
      }
      html = result2
1240 1241 1242 1243 1244 1245
    } catch (err) {
      console.error(err)
      res.statusCode = 500
      html = 'Internal Server Error'
    }
    return html
N
Naoyuki Kanezawa 已提交
1246 1247
  }

J
Joe Haddad 已提交
1248 1249 1250
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
1251
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1252
  ): Promise<void> {
1253 1254
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
N
Naoyuki Kanezawa 已提交
1255
    res.statusCode = 404
1256
    return this.renderError(null, req, res, pathname!, query)
N
Naoyuki Kanezawa 已提交
1257
  }
N
Naoyuki Kanezawa 已提交
1258

J
Joe Haddad 已提交
1259 1260 1261 1262
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
1263
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1264
  ): Promise<void> {
A
Arunoda Susiripala 已提交
1265
    if (!this.isServeableUrl(path)) {
1266
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
1267 1268
    }

1269 1270 1271 1272 1273 1274
    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 已提交
1275
    try {
1276
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
1277
    } catch (err) {
T
Tim Neutkens 已提交
1278
      if (err.code === 'ENOENT' || err.statusCode === 404) {
1279
        this.render404(req, res, parsedUrl)
1280 1281 1282
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
1283 1284 1285 1286 1287 1288
      } else {
        throw err
      }
    }
  }

1289
  private isServeableUrl(path: string): boolean {
A
Arunoda Susiripala 已提交
1290 1291
    const resolved = resolve(path)
    if (
1292
      resolved.indexOf(join(this.distDir) + sep) !== 0 &&
1293 1294
      resolved.indexOf(join(this.dir, 'static') + sep) !== 0 &&
      resolved.indexOf(join(this.dir, 'public') + sep) !== 0
A
Arunoda Susiripala 已提交
1295 1296 1297 1298 1299 1300 1301 1302
    ) {
      // Seems like the user is trying to traverse the filesystem.
      return false
    }

    return true
  }

1303
  protected readBuildId(): string {
1304 1305 1306 1307 1308
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
1309
        throw new Error(
1310
          `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 已提交
1311
        )
1312 1313 1314
      }

      throw err
1315
    }
1316
  }
1317 1318 1319 1320

  private get _isLikeServerless(): boolean {
    return isTargetLikeServerless(this.nextConfig.target)
  }
1321
}
1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 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

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