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

const getCustomRouteMatcher = pathMatch(true)
64 65 66

type NextConfig = any

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

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

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

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

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

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

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

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

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

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

179 180 181 182 183 184 185 186 187 188
    this.serverBuildDir = join(
      this.distDir,
      this._isLikeServerless ? SERVERLESS_DIRECTORY : SERVER_DIRECTORY
    )
    const pagesManifestPath = join(this.serverBuildDir, PAGES_MANIFEST)

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

J
JJ Kasper 已提交
189
    this.router = new Router(this.generateRoutes())
190
    this.setAssetPrefix(assetPrefix)
J
JJ Kasper 已提交
191

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

J
JJ Kasper 已提交
204 205 206 207 208 209 210 211 212 213 214 215
    initializeSprCache({
      dev,
      distDir: this.distDir,
      pagesDir: join(
        this.distDir,
        this._isLikeServerless
          ? SERVERLESS_DIRECTORY
          : `${SERVER_DIRECTORY}/static/${this.buildId}`,
        'pages'
      ),
      flushToDisk: this.nextConfig.experimental.sprFlushToDisk,
    })
N
Naoyuki Kanezawa 已提交
216
  }
N
nkzawa 已提交
217

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

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

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

242 243 244
    // Parse the querystring ourselves if the user doesn't handle querystring parsing
    if (typeof parsedUrl.query === 'string') {
      parsedUrl.query = parseQs(parsedUrl.query)
N
Naoyuki Kanezawa 已提交
245
    }
246

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

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

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

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

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

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

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

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

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

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

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

314
    const staticFilesRoute = this.hasStaticDir
315 316 317 318 319 320 321
      ? [
          {
            // It's very important to keep this route's param optional.
            // (but it should support as many params as needed, separated by '/')
            // Otherwise this will lead to a pretty simple DOS attack.
            // See more: https://github.com/zeit/next.js/issues/2617
            match: route('/static/:path*'),
322
            name: 'static catchall',
323 324 325
            fn: async (req, res, params, parsedUrl) => {
              const p = join(this.dir, 'static', ...(params.path || []))
              await this.serveStatic(req, res, p, parsedUrl)
326 327 328
              return {
                finished: true,
              }
329 330 331 332
            },
          } as Route,
        ]
      : []
333

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

338
    const fsRoutes: Route[] = [
T
Tim Neutkens 已提交
339
      {
340
        match: route('/_next/static/:path*'),
341 342
        type: 'route',
        name: '_next/static catchall',
343
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
344 345 346
          // The commons folder holds commonschunk files
          // The chunks folder holds dynamic entries
          // The buildId folder holds pages and potentially other assets. As buildId changes per build it can be long-term cached.
347 348

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

J
Joe Haddad 已提交
356 357 358
          if (
            params.path[0] === CLIENT_STATIC_FILES_RUNTIME ||
            params.path[0] === 'chunks' ||
359 360
            params.path[0] === 'css' ||
            params.path[0] === 'media' ||
J
Joe Haddad 已提交
361 362
            params.path[0] === this.buildId
          ) {
T
Tim Neutkens 已提交
363
            this.setImmutableAssetCacheControl(res)
364
          }
J
Joe Haddad 已提交
365 366 367
          const p = join(
            this.distDir,
            CLIENT_STATIC_FILES_PATH,
368
            ...(params.path || [])
J
Joe Haddad 已提交
369
          )
370
          await this.serveStatic(req, res, p, parsedUrl)
371 372 373
          return {
            finished: true,
          }
374
        },
375
      },
J
JJ Kasper 已提交
376 377
      {
        match: route('/_next/data/:path*'),
378 379
        type: 'route',
        name: '_next/data catchall',
J
JJ Kasper 已提交
380
        fn: async (req, res, params, _parsedUrl) => {
J
JJ Kasper 已提交
381 382 383
          // Make sure to 404 for /_next/data/ itself and
          // we also want to 404 if the buildId isn't correct
          if (!params.path || params.path[0] !== this.buildId) {
384 385 386 387
            await this.render404(req, res, _parsedUrl)
            return {
              finished: true,
            }
J
JJ Kasper 已提交
388 389 390 391 392 393
          }
          // remove buildId from URL
          params.path.shift()

          // show 404 if it doesn't end with .json
          if (!params.path[params.path.length - 1].endsWith('.json')) {
394 395 396 397
            await this.render404(req, res, _parsedUrl)
            return {
              finished: true,
            }
J
JJ Kasper 已提交
398 399 400 401 402 403 404
          }

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

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

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

448
      // Headers come very first
449 450 451 452 453 454 455 456 457 458 459 460 461 462
      headers = this.customRoutes.headers.map(r => {
        const route = getCustomRoute(r, 'header')
        return {
          match: route.match,
          type: route.type,
          name: `${route.type} ${route.source} header route`,
          fn: async (_req, res, _params, _parsedUrl) => {
            for (const header of (route as Header).headers) {
              res.setHeader(header.key, header.value)
            }
            return { finished: false }
          },
        } as Route
      })
J
JJ Kasper 已提交
463

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

            res.setHeader('Location', updatedDestination)
            res.statusCode = getRedirectStatus(route as Redirect)

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

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

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

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

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

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

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

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

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

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

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

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

584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601
  private async getPagePath(pathname: string) {
    return getPagePath(
      pathname,
      this.distDir,
      this._isLikeServerless,
      this.renderOpts.dev
    )
  }

  protected async hasPage(pathname: string): Promise<boolean> {
    let found = false
    try {
      found = !!(await this.getPagePath(pathname))
    } catch (_) {}

    return found
  }

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

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

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

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

641
    if (!pageFound) {
642
      return false
J
JJ Kasper 已提交
643
    }
644 645 646 647 648 649
    // Make sure the page is built before getting the path
    // or else it won't be in the manifest yet
    await this.ensureApiPage(page)

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

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

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

672
  protected generatePublicRoutes(): Route[] {
673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691
    const publicFiles = new Set(
      recursiveReadDirSync(this.publicDir).map(p => p.replace(/\\/g, '/'))
    )

    return [
      {
        match: route('/:path*'),
        name: 'public folder catchall',
        fn: async (req, res, params, parsedUrl) => {
          const path = `/${(params.path || []).join('/')}`

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

J
Joe Haddad 已提交
995 996 997 998 999 1000 1001 1002 1003 1004
    // render fallback if for a preview path or a non-seeded dynamic path
    const isDynamicPathname = isDynamicRoute(pathname)
    if (
      !isResSent(res) &&
      !isDataReq &&
      ((isPreviewMode &&
        // A header can opt into the blocking behavior.
        req.headers['X-Prerender-Bypass-Mode'] !== 'Blocking') ||
        isDynamicPathname)
    ) {
1005 1006
      let html = ''

J
Joe Haddad 已提交
1007 1008
      const isProduction = !this.renderOpts.dev
      if (isProduction && (isDynamicPathname || !isPreviewMode)) {
1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025
        html = await getFallback(pathname)
      } else {
        query.__nextFallback = 'true'
        if (isLikeServerless) {
          this.prepareServerlessUrl(req, query)
          html = await (result.Component as any).renderReqToHTML(req, res)
        } else {
          html = (await renderToHTML(req, res, pathname, query, {
            ...result,
            ...opts,
          })) as string
        }
      }

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

1026 1027
    return doRender(ssgCacheKey, []).then(
      async ({ isOrigin, value: { html, pageData, sprRevalidate } }) => {
J
JJ Kasper 已提交
1028 1029 1030 1031
        // Respond to the request if a payload wasn't sent above (from cache)
        if (!isResSent(res)) {
          this.__sendPayload(
            res,
1032 1033
            isDataReq ? JSON.stringify(pageData) : html,
            isDataReq ? 'application/json' : 'text/html; charset=utf-8',
J
JJ Kasper 已提交
1034
            sprRevalidate
J
JJ Kasper 已提交
1035 1036 1037 1038 1039
          )
        }

        // Update the SPR cache if the head request
        if (isOrigin) {
J
Joe Haddad 已提交
1040 1041 1042 1043 1044 1045 1046 1047
          // Preview mode should not be stored in cache
          if (!isPreviewMode) {
            await setSprCache(
              ssgCacheKey,
              { html: html!, pageData },
              sprRevalidate
            )
          }
J
JJ Kasper 已提交
1048 1049 1050 1051 1052
        }

        return null
      }
    )
1053 1054
  }

J
Joe Haddad 已提交
1055
  public renderToHTML(
J
Joe Haddad 已提交
1056 1057 1058 1059
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
J
Joe Haddad 已提交
1060 1061 1062 1063 1064 1065
    {
      amphtml,
      hasAmp,
    }: {
      amphtml?: boolean
      hasAmp?: boolean
1066
    } = {}
J
Joe Haddad 已提交
1067
  ): Promise<string | null> {
J
Joe Haddad 已提交
1068 1069
    return this.findPageComponents(pathname, query)
      .then(
1070
        result => {
J
Joe Haddad 已提交
1071 1072 1073 1074
          return this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
1075
            result.unstable_getStaticProps
1076
              ? { _nextDataReq: query._nextDataReq }
1077
              : query,
J
Joe Haddad 已提交
1078
            result,
T
Tim Neutkens 已提交
1079
            { ...this.renderOpts, amphtml, hasAmp }
J
Joe Haddad 已提交
1080 1081
          )
        },
1082
        err => {
J
Joe Haddad 已提交
1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093
          if (err.code !== 'ENOENT' || !this.dynamicRoutes) {
            return Promise.reject(err)
          }

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

            return this.findPageComponents(dynamicRoute.page, query).then(
1094 1095
              result => {
                return this.renderToHTMLWithComponents(
J
Joe Haddad 已提交
1096 1097 1098
                  req,
                  res,
                  dynamicRoute.page,
J
JJ Kasper 已提交
1099 1100 1101
                  // only add params for SPR enabled pages
                  {
                    ...(result.unstable_getStaticProps
1102
                      ? { _nextDataReq: query._nextDataReq }
J
JJ Kasper 已提交
1103 1104 1105
                      : query),
                    ...params,
                  },
J
Joe Haddad 已提交
1106
                  result,
J
JJ Kasper 已提交
1107 1108
                  {
                    ...this.renderOpts,
1109
                    params,
J
JJ Kasper 已提交
1110 1111 1112
                    amphtml,
                    hasAmp,
                  }
1113
                )
1114
              }
J
Joe Haddad 已提交
1115 1116 1117 1118
            )
          }

          return Promise.reject(err)
1119
        }
J
Joe Haddad 已提交
1120
      )
1121
      .catch(err => {
J
Joe Haddad 已提交
1122 1123 1124 1125 1126 1127 1128 1129 1130
        if (err && err.code === 'ENOENT') {
          res.statusCode = 404
          return this.renderErrorToHTML(null, req, res, pathname, query)
        } else {
          this.logError(err)
          res.statusCode = 500
          return this.renderErrorToHTML(err, req, res, pathname, query)
        }
      })
N
Naoyuki Kanezawa 已提交
1131 1132
  }

J
Joe Haddad 已提交
1133 1134 1135 1136 1137
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1138
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1139 1140 1141
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
1142
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
1143
    )
N
Naoyuki Kanezawa 已提交
1144
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
1145
    if (html === null) {
1146 1147
      return
    }
1148
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
1149 1150
  }

J
Joe Haddad 已提交
1151 1152 1153 1154 1155
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
1156
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1157
  ) {
1158 1159
    let result: null | LoadComponentsReturnType = null

1160 1161 1162 1163
    const { static404, pages404 } = this.nextConfig.experimental
    const is404 = res.statusCode === 404
    let using404Page = false

1164
    // use static 404 page if available and is 404 response
1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185
    if (is404) {
      if (static404) {
        try {
          result = await this.findPageComponents('/_errors/404')
        } catch (err) {
          if (err.code !== 'ENOENT') {
            throw err
          }
        }
      }

      // use 404 if /_errors/404 isn't available which occurs
      // during development and when _app has getInitialProps
      if (!result && pages404) {
        try {
          result = await this.findPageComponents('/404')
          using404Page = true
        } catch (err) {
          if (err.code !== 'ENOENT') {
            throw err
          }
1186 1187 1188 1189 1190 1191 1192 1193
        }
      }
    }

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

1194 1195 1196 1197 1198
    let html
    try {
      html = await this.renderToHTMLWithComponents(
        req,
        res,
1199
        using404Page ? '/404' : '/_error',
1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212
        query,
        result,
        {
          ...this.renderOpts,
          err,
        }
      )
    } catch (err) {
      console.error(err)
      res.statusCode = 500
      html = 'Internal Server Error'
    }
    return html
N
Naoyuki Kanezawa 已提交
1213 1214
  }

J
Joe Haddad 已提交
1215 1216 1217
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
1218
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1219
  ): Promise<void> {
1220 1221
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
1222
    if (!pathname) {
1223 1224
      throw new Error('pathname is undefined')
    }
N
Naoyuki Kanezawa 已提交
1225
    res.statusCode = 404
1226
    return this.renderError(null, req, res, pathname, query)
N
Naoyuki Kanezawa 已提交
1227
  }
N
Naoyuki Kanezawa 已提交
1228

J
Joe Haddad 已提交
1229 1230 1231 1232
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
1233
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1234
  ): Promise<void> {
A
Arunoda Susiripala 已提交
1235
    if (!this.isServeableUrl(path)) {
1236
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
1237 1238
    }

1239 1240 1241 1242 1243 1244
    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 已提交
1245
    try {
1246
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
1247
    } catch (err) {
T
Tim Neutkens 已提交
1248
      if (err.code === 'ENOENT' || err.statusCode === 404) {
1249
        this.render404(req, res, parsedUrl)
1250 1251 1252
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
1253 1254 1255 1256 1257 1258
      } else {
        throw err
      }
    }
  }

1259
  private isServeableUrl(path: string): boolean {
A
Arunoda Susiripala 已提交
1260 1261
    const resolved = resolve(path)
    if (
1262
      resolved.indexOf(join(this.distDir) + sep) !== 0 &&
1263 1264
      resolved.indexOf(join(this.dir, 'static') + sep) !== 0 &&
      resolved.indexOf(join(this.dir, 'public') + sep) !== 0
A
Arunoda Susiripala 已提交
1265 1266 1267 1268 1269 1270 1271 1272
    ) {
      // Seems like the user is trying to traverse the filesystem.
      return false
    }

    return true
  }

1273
  protected readBuildId(): string {
1274 1275 1276 1277 1278
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
1279
        throw new Error(
1280
          `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 已提交
1281
        )
1282 1283 1284
      }

      throw err
1285
    }
1286
  }
1287 1288 1289 1290

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