next-server.ts 35.6 KB
Newer Older
1
import compression from 'compression'
J
Joe Haddad 已提交
2
import fs from 'fs'
3
import Proxy from 'http-proxy'
J
Joe Haddad 已提交
4
import { IncomingMessage, ServerResponse } from 'http'
J
Joe Haddad 已提交
5
import { join, resolve, sep } from 'path'
J
Joe Haddad 已提交
6
import { compile as compilePathToRegex } from 'path-to-regexp'
7
import { parse as parseQs, ParsedUrlQuery } from 'querystring'
8
import { format as formatUrl, parse as parseUrl, UrlWithParsedQuery } from 'url'
J
Joe Haddad 已提交
9

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

const getCustomRouteMatcher = pathMatch(true)
63 64 65

type NextConfig = any

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

T
Tim Neutkens 已提交
246 247 248 249 250 251 252 253 254 255
    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, '')
    }

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

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

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

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

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

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

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

286 287 288 289 290 291 292
  protected generateRoutes(): {
    routes: Route[]
    fsRoutes: Route[]
    catchAllRoute: Route
    pageChecker: PageChecker
    dynamicRoutes: DynamicRoutes | undefined
  } {
J
JJ Kasper 已提交
293 294
    this.customRoutes = this.getCustomRoutes()

295 296 297
    const publicRoutes = fs.existsSync(this.publicDir)
      ? this.generatePublicRoutes()
      : []
J
JJ Kasper 已提交
298

299
    const staticFilesRoute = this.hasStaticDir
300 301 302 303 304 305 306
      ? [
          {
            // 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*'),
307
            name: 'static catchall',
308 309 310
            fn: async (req, res, params, parsedUrl) => {
              const p = join(this.dir, 'static', ...(params.path || []))
              await this.serveStatic(req, res, p, parsedUrl)
311 312 313
              return {
                finished: true,
              }
314 315 316 317
            },
          } as Route,
        ]
      : []
318

319
    const fsRoutes: Route[] = [
T
Tim Neutkens 已提交
320
      {
321
        match: route('/_next/static/:path*'),
322 323
        type: 'route',
        name: '_next/static catchall',
324
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
325 326 327
          // 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.
328 329

          // make sure to 404 for /_next/static itself
330 331 332 333 334 335
          if (!params.path) {
            await this.render404(req, res, parsedUrl)
            return {
              finished: true,
            }
          }
336

J
Joe Haddad 已提交
337 338 339
          if (
            params.path[0] === CLIENT_STATIC_FILES_RUNTIME ||
            params.path[0] === 'chunks' ||
340 341
            params.path[0] === 'css' ||
            params.path[0] === 'media' ||
J
Joe Haddad 已提交
342 343
            params.path[0] === this.buildId
          ) {
T
Tim Neutkens 已提交
344
            this.setImmutableAssetCacheControl(res)
345
          }
J
Joe Haddad 已提交
346 347 348
          const p = join(
            this.distDir,
            CLIENT_STATIC_FILES_PATH,
349
            ...(params.path || [])
J
Joe Haddad 已提交
350
          )
351
          await this.serveStatic(req, res, p, parsedUrl)
352 353 354
          return {
            finished: true,
          }
355
        },
356
      },
J
JJ Kasper 已提交
357 358
      {
        match: route('/_next/data/:path*'),
359 360
        type: 'route',
        name: '_next/data catchall',
J
JJ Kasper 已提交
361
        fn: async (req, res, params, _parsedUrl) => {
J
JJ Kasper 已提交
362 363 364
          // 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) {
365 366 367 368
            await this.render404(req, res, _parsedUrl)
            return {
              finished: true,
            }
J
JJ Kasper 已提交
369 370 371 372 373 374
          }
          // 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')) {
375 376 377 378
            await this.render404(req, res, _parsedUrl)
            return {
              finished: true,
            }
J
JJ Kasper 已提交
379 380 381 382 383 384 385
          }

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

J
JJ Kasper 已提交
386 387 388 389 390 391
          req.url = pathname
          const parsedUrl = parseUrl(pathname, true)
          await this.render(
            req,
            res,
            pathname,
392
            { ..._parsedUrl.query, _nextDataReq: '1' },
J
JJ Kasper 已提交
393 394
            parsedUrl
          )
395 396 397
          return {
            finished: true,
          }
J
JJ Kasper 已提交
398 399
        },
      },
T
Tim Neutkens 已提交
400
      {
401
        match: route('/_next/:path*'),
402 403
        type: 'route',
        name: '_next catchall',
T
Tim Neutkens 已提交
404
        // This path is needed because `render()` does a check for `/_next` and the calls the routing again
405
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
406
          await this.render404(req, res, parsedUrl)
407 408 409
          return {
            finished: true,
          }
L
Lukáš Huvar 已提交
410 411
        },
      },
412 413
      ...publicRoutes,
      ...staticFilesRoute,
T
Tim Neutkens 已提交
414
    ]
415
    const routes: Route[] = []
416

J
JJ Kasper 已提交
417
    if (this.customRoutes) {
418
      const { redirects, rewrites, headers } = this.customRoutes
J
JJ Kasper 已提交
419 420

      const getCustomRoute = (
421 422
        r: Rewrite | Redirect | Header,
        type: RouteType
J
JJ Kasper 已提交
423 424 425 426 427 428
      ) => ({
        ...r,
        type,
        matcher: getCustomRouteMatcher(r.source),
      })

429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447
      // Headers come very first
      routes.push(
        ...headers.map(r => {
          const route = getCustomRoute(r, 'header')
          return {
            check: true,
            match: route.matcher,
            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 已提交
448 449 450 451 452 453
      const customRoutes = [
        ...redirects.map(r => getCustomRoute(r, 'redirect')),
        ...rewrites.map(r => getCustomRoute(r, 'rewrite')),
      ]

      routes.push(
454
        ...customRoutes.map(route => {
J
JJ Kasper 已提交
455
          return {
456
            check: true,
457 458
            match: route.matcher,
            type: route.type,
459
            statusCode: (route as Redirect).statusCode,
460
            name: `${route.type} ${route.source} route`,
461
            fn: async (req, res, params, _parsedUrl) => {
462
              const parsedDestination = parseUrl(route.destination, true)
463
              const destQuery = parsedDestination.query
464 465 466
              let destinationCompiler = compilePathToRegex(
                `${parsedDestination.pathname!}${parsedDestination.hash || ''}`
              )
467 468
              let newUrl

469 470 471 472 473 474 475 476 477 478 479
              Object.keys(destQuery).forEach(key => {
                const val = destQuery[key]
                if (
                  typeof val === 'string' &&
                  val.startsWith(':') &&
                  params[val.substr(1)]
                ) {
                  destQuery[key] = params[val.substr(1)]
                }
              })

480 481 482 483
              try {
                newUrl = destinationCompiler(params)
              } catch (err) {
                if (
J
Joe Haddad 已提交
484 485 486
                  err.message.match(
                    /Expected .*? to not repeat, but got an array/
                  )
487 488 489 490 491 492 493
                ) {
                  throw new Error(
                    `To use a multi-match in the destination you must add \`*\` at the end of the param name to signify it should repeat. https://err.sh/zeit/next.js/invalid-multi-match`
                  )
                }
                throw err
              }
494

495 496 497 498 499 500 501
              const parsedNewUrl = parseUrl(newUrl)
              const updatedDestination = formatUrl({
                ...parsedDestination,
                pathname: parsedNewUrl.pathname,
                hash: parsedNewUrl.hash,
                search: undefined,
              })
502

503
              if (route.type === 'redirect') {
504
                res.setHeader('Location', updatedDestination)
505
                res.statusCode = getRedirectStatus(route as Redirect)
506 507 508 509 510 511 512

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

513 514 515
                res.end()
                return {
                  finished: true,
J
JJ Kasper 已提交
516
                }
517
              } else {
518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537
                // external rewrite, proxy it
                if (parsedDestination.protocol) {
                  const proxy = new Proxy({
                    target: updatedDestination,
                    changeOrigin: true,
                    ignorePath: true,
                  })
                  proxy.web(req, res)

                  proxy.on('error', (err: Error) => {
                    console.error(
                      `Error occurred proxying ${updatedDestination}`,
                      err
                    )
                  })
                  return {
                    finished: true,
                  }
                }
                ;(req as any)._nextDidRewrite = true
J
JJ Kasper 已提交
538 539
              }

540 541 542
              return {
                finished: false,
                pathname: newUrl,
543
                query: parsedDestination.query,
J
JJ Kasper 已提交
544 545 546 547 548 549 550
              }
            },
          } as Route
        })
      )
    }

551
    const catchPublicDirectoryRoute: Route = {
552
      match: route('/:path*'),
553
      type: 'route',
554
      name: 'Catch public directory route',
555
      fn: async (req, res, params, parsedUrl) => {
556
        const { pathname } = parsedUrl
557 558 559 560 561 562 563 564 565 566 567
        if (!pathname) {
          throw new Error('pathname is undefined')
        }

        // Used in development to check public directory paths
        if (await this._beforeCatchAllRender(req, res, params, parsedUrl)) {
          return {
            finished: true,
          }
        }

568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585
        return {
          finished: false,
        }
      },
    }

    routes.push(catchPublicDirectoryRoute)

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

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

        await this.render(req, res, pathname, query, parsedUrl)
599 600 601 602
        return {
          finished: true,
        }
      },
603
    }
604

605
    if (this.nextConfig.useFileSystemPublicRoutes) {
J
Joe Haddad 已提交
606
      this.dynamicRoutes = this.getDynamicRoutes()
J
Joe Haddad 已提交
607

608 609 610
      // 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.
611
      // See more: https://github.com/zeit/next.js/issues/2617
612
      routes.push(catchAllRoute)
613
    }
N
nkzawa 已提交
614

615 616 617 618 619 620 621
    return {
      routes,
      fsRoutes,
      catchAllRoute,
      dynamicRoutes: this.dynamicRoutes,
      pageChecker: this.hasPage.bind(this),
    }
T
Tim Neutkens 已提交
622 623
  }

624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641
  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
  }

642 643 644 645 646 647 648 649 650
  protected async _beforeCatchAllRender(
    _req: IncomingMessage,
    _res: ServerResponse,
    _params: Params,
    _parsedUrl: UrlWithParsedQuery
  ) {
    return false
  }

651 652 653
  // Used to build API page in development
  protected async ensureApiPage(pathname: string) {}

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

670
    if (!pageFound && this.dynamicRoutes) {
L
Lukáš Huvar 已提交
671 672
      for (const dynamicRoute of this.dynamicRoutes) {
        params = dynamicRoute.match(pathname)
673
        if (dynamicRoute.page.startsWith('/api') && params) {
674 675
          page = dynamicRoute.page
          pageFound = true
L
Lukáš Huvar 已提交
676 677 678 679 680
          break
        }
      }
    }

681
    if (!pageFound) {
682
      return false
J
JJ Kasper 已提交
683
    }
684 685 686 687 688 689
    // 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)
690
    query = { ...query, ...params }
J
JJ Kasper 已提交
691

692
    if (!this.renderOpts.dev && this._isLikeServerless) {
693
      if (typeof pageModule.default === 'function') {
694
        this.prepareServerlessUrl(req, query)
695 696
        await pageModule.default(req, res)
        return true
J
JJ Kasper 已提交
697 698 699
      }
    }

700
    await apiResolver(req, res, query, pageModule, this.onErrorMiddleware)
701
    return true
L
Lukáš Huvar 已提交
702 703
  }

704
  protected generatePublicRoutes(): Route[] {
705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723
    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
            )
724 725 726
            return {
              finished: true,
            }
727 728 729 730 731 732 733
          }
          return {
            finished: false,
          }
        },
      } as Route,
    ]
734 735
  }

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

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

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

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

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

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

J
Joe Haddad 已提交
784 785 786 787 788
  public async render(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
789
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
790
  ): Promise<void> {
791
    const url: any = req.url
792 793 794 795 796

    if (
      url.match(/^\/_next\//) ||
      (this.hasStaticDir && url.match(/^\/static\//))
    ) {
797 798 799
      return this.handleRequest(req, res, parsedUrl)
    }

800
    if (isBlockedPage(pathname)) {
801
      return this.render404(req, res, parsedUrl)
802 803
    }

T
Tim Neutkens 已提交
804
    const html = await this.renderToHTML(req, res, pathname, query, {})
805 806
    // Request was ended by the user
    if (html === null) {
807 808 809
      return
    }

810
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
811
  }
N
nkzawa 已提交
812

J
Joe Haddad 已提交
813
  private async findPageComponents(
J
Joe Haddad 已提交
814
    pathname: string,
815
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
816
  ) {
817
    const serverless = !this.renderOpts.dev && this._isLikeServerless
J
JJ Kasper 已提交
818 819 820
    // try serving a static AMP version first
    if (query.amp) {
      try {
J
Joe Haddad 已提交
821 822 823
        return await loadComponents(
          this.distDir,
          this.buildId,
824
          normalizePagePath(pathname) + '.amp',
825
          serverless
J
Joe Haddad 已提交
826
        )
J
JJ Kasper 已提交
827 828 829 830
      } catch (err) {
        if (err.code !== 'ENOENT') throw err
      }
    }
J
Joe Haddad 已提交
831 832 833 834
    return await loadComponents(
      this.distDir,
      this.buildId,
      pathname,
835
      serverless
J
Joe Haddad 已提交
836 837 838
    )
  }

J
JJ Kasper 已提交
839 840 841 842 843 844
  private __sendPayload(
    res: ServerResponse,
    payload: any,
    type: string,
    revalidate?: number | false
  ) {
J
JJ Kasper 已提交
845
    // TODO: ETag? Cache-Control headers? Next-specific headers?
J
JJ Kasper 已提交
846
    res.setHeader('Content-Type', type)
J
JJ Kasper 已提交
847
    res.setHeader('Content-Length', Buffer.byteLength(payload))
J
Joe Haddad 已提交
848 849 850 851
    if (!this.renderOpts.dev) {
      if (revalidate) {
        res.setHeader(
          'Cache-Control',
852 853 854
          revalidate < 0
            ? `no-cache, no-store, must-revalidate`
            : `s-maxage=${revalidate}, stale-while-revalidate`
J
Joe Haddad 已提交
855 856 857 858 859 860 861
        )
      } else if (revalidate === false) {
        res.setHeader(
          'Cache-Control',
          `s-maxage=31536000, stale-while-revalidate`
        )
      }
J
JJ Kasper 已提交
862
    }
J
JJ Kasper 已提交
863 864 865
    res.end(payload)
  }

866 867 868 869 870 871 872 873 874 875 876 877
  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 已提交
878 879 880 881 882 883
  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
    result: LoadComponentsReturnType,
884
    opts: any
J
JJ Kasper 已提交
885
  ): Promise<string | null> {
886 887 888 889 890
    // 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 已提交
891
    // handle static page
J
Joe Haddad 已提交
892 893 894 895
    if (typeof result.Component === 'string') {
      return result.Component
    }

J
JJ Kasper 已提交
896 897
    // check request state
    const isLikeServerless =
J
Joe Haddad 已提交
898
      typeof result.Component === 'object' &&
899
      typeof (result.Component as any).renderReqToHTML === 'function'
900
    const isSSG = !!result.unstable_getStaticProps
901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916
    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 已提交
917 918

    // non-spr requests should render like normal
919
    if (!isSSG) {
J
JJ Kasper 已提交
920 921
      // handle serverless
      if (isLikeServerless) {
922 923 924 925 926 927 928 929 930 931 932 933 934 935 936
        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
        }
937
        this.prepareServerlessUrl(req, query)
938
        return (result.Component as any).renderReqToHTML(req, res)
J
JJ Kasper 已提交
939 940
      }

941 942 943 944 945 946 947 948 949 950
      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 已提交
951 952 953 954 955 956 957
      return renderToHTML(req, res, pathname, query, {
        ...result,
        ...opts,
      })
    }

    // Compute the SPR cache key
958
    const ssgCacheKey = parseUrl(req.url || '').pathname!
J
JJ Kasper 已提交
959 960

    // Complete the response with cached data if its present
961
    const cachedData = await getSprCache(ssgCacheKey)
J
JJ Kasper 已提交
962
    if (cachedData) {
963
      const data = isDataReq
J
JJ Kasper 已提交
964 965 966 967 968 969
        ? JSON.stringify(cachedData.pageData)
        : cachedData.html

      this.__sendPayload(
        res,
        data,
970
        isDataReq ? 'application/json' : 'text/html; charset=utf-8',
J
JJ Kasper 已提交
971
        cachedData.curRevalidate
J
JJ Kasper 已提交
972 973 974 975 976 977
      )

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

J
JJ Kasper 已提交
980 981 982 983
    // If we're here, that means data is missing or it's stale.

    const doRender = withCoalescedInvoke(async function(): Promise<{
      html: string | null
984
      pageData: any
J
JJ Kasper 已提交
985 986
      sprRevalidate: number | false
    }> {
987
      let pageData: any
J
JJ Kasper 已提交
988 989 990 991 992 993
      let html: string | null
      let sprRevalidate: number | false

      let renderResult
      // handle serverless
      if (isLikeServerless) {
994 995 996 997 998
        renderResult = await (result.Component as any).renderReqToHTML(
          req,
          res,
          true
        )
J
JJ Kasper 已提交
999 1000

        html = renderResult.html
1001
        pageData = renderResult.renderOpts.pageData
J
JJ Kasper 已提交
1002 1003 1004 1005 1006 1007 1008 1009 1010
        sprRevalidate = renderResult.renderOpts.revalidate
      } else {
        const renderOpts = {
          ...result,
          ...opts,
        }
        renderResult = await renderToHTML(req, res, pathname, query, renderOpts)

        html = renderResult
1011
        pageData = renderOpts.pageData
J
JJ Kasper 已提交
1012 1013 1014
        sprRevalidate = renderOpts.revalidate
      }

1015
      return { html, pageData, sprRevalidate }
1016
    })
J
JJ Kasper 已提交
1017

1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039
    // render fallback if cached data wasn't available
    if (!isResSent(res) && !isDataReq && isDynamicRoute(pathname)) {
      let html = ''

      if (!this.renderOpts.dev) {
        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')
    }

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

        // Update the SPR cache if the head request
        if (isOrigin) {
          await setSprCache(
1055 1056
            ssgCacheKey,
            { html: html!, pageData },
J
JJ Kasper 已提交
1057 1058 1059 1060 1061 1062 1063
            sprRevalidate
          )
        }

        return null
      }
    )
1064 1065
  }

J
Joe Haddad 已提交
1066
  public renderToHTML(
J
Joe Haddad 已提交
1067 1068 1069 1070
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
J
Joe Haddad 已提交
1071 1072 1073 1074 1075 1076
    {
      amphtml,
      hasAmp,
    }: {
      amphtml?: boolean
      hasAmp?: boolean
1077
    } = {}
J
Joe Haddad 已提交
1078
  ): Promise<string | null> {
J
Joe Haddad 已提交
1079 1080
    return this.findPageComponents(pathname, query)
      .then(
1081
        result => {
J
Joe Haddad 已提交
1082 1083 1084 1085
          return this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
1086
            result.unstable_getStaticProps
1087
              ? { _nextDataReq: query._nextDataReq }
1088
              : query,
J
Joe Haddad 已提交
1089
            result,
T
Tim Neutkens 已提交
1090
            { ...this.renderOpts, amphtml, hasAmp }
J
Joe Haddad 已提交
1091 1092
          )
        },
1093
        err => {
J
Joe Haddad 已提交
1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104
          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(
1105 1106
              result => {
                return this.renderToHTMLWithComponents(
J
Joe Haddad 已提交
1107 1108 1109
                  req,
                  res,
                  dynamicRoute.page,
J
JJ Kasper 已提交
1110 1111 1112
                  // only add params for SPR enabled pages
                  {
                    ...(result.unstable_getStaticProps
1113
                      ? { _nextDataReq: query._nextDataReq }
J
JJ Kasper 已提交
1114 1115 1116
                      : query),
                    ...params,
                  },
J
Joe Haddad 已提交
1117
                  result,
J
JJ Kasper 已提交
1118 1119
                  {
                    ...this.renderOpts,
1120
                    params,
J
JJ Kasper 已提交
1121 1122 1123
                    amphtml,
                    hasAmp,
                  }
1124
                )
1125
              }
J
Joe Haddad 已提交
1126 1127 1128 1129
            )
          }

          return Promise.reject(err)
1130
        }
J
Joe Haddad 已提交
1131
      )
1132
      .catch(err => {
J
Joe Haddad 已提交
1133 1134 1135 1136 1137 1138 1139 1140 1141
        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 已提交
1142 1143
  }

J
Joe Haddad 已提交
1144 1145 1146 1147 1148
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1149
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1150 1151 1152
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
1153
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
1154
    )
N
Naoyuki Kanezawa 已提交
1155
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
1156
    if (html === null) {
1157 1158
      return
    }
1159
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
1160 1161
  }

J
Joe Haddad 已提交
1162 1163 1164 1165 1166
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
1167
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1168
  ) {
1169 1170
    let result: null | LoadComponentsReturnType = null

1171 1172 1173 1174
    const { static404, pages404 } = this.nextConfig.experimental
    const is404 = res.statusCode === 404
    let using404Page = false

1175
    // use static 404 page if available and is 404 response
1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196
    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
          }
1197 1198 1199 1200 1201 1202 1203 1204
        }
      }
    }

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

1205 1206 1207 1208 1209
    let html
    try {
      html = await this.renderToHTMLWithComponents(
        req,
        res,
1210
        using404Page ? '/404' : '/_error',
1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223
        query,
        result,
        {
          ...this.renderOpts,
          err,
        }
      )
    } catch (err) {
      console.error(err)
      res.statusCode = 500
      html = 'Internal Server Error'
    }
    return html
N
Naoyuki Kanezawa 已提交
1224 1225
  }

J
Joe Haddad 已提交
1226 1227 1228
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
1229
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1230
  ): Promise<void> {
1231 1232
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
1233
    if (!pathname) {
1234 1235
      throw new Error('pathname is undefined')
    }
N
Naoyuki Kanezawa 已提交
1236
    res.statusCode = 404
1237
    return this.renderError(null, req, res, pathname, query)
N
Naoyuki Kanezawa 已提交
1238
  }
N
Naoyuki Kanezawa 已提交
1239

J
Joe Haddad 已提交
1240 1241 1242 1243
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
1244
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1245
  ): Promise<void> {
A
Arunoda Susiripala 已提交
1246
    if (!this.isServeableUrl(path)) {
1247
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
1248 1249
    }

1250 1251 1252 1253 1254 1255
    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 已提交
1256
    try {
1257
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
1258
    } catch (err) {
T
Tim Neutkens 已提交
1259
      if (err.code === 'ENOENT' || err.statusCode === 404) {
1260
        this.render404(req, res, parsedUrl)
1261 1262 1263
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
1264 1265 1266 1267 1268 1269
      } else {
        throw err
      }
    }
  }

1270
  private isServeableUrl(path: string): boolean {
A
Arunoda Susiripala 已提交
1271 1272
    const resolved = resolve(path)
    if (
1273
      resolved.indexOf(join(this.distDir) + sep) !== 0 &&
1274 1275
      resolved.indexOf(join(this.dir, 'static') + sep) !== 0 &&
      resolved.indexOf(join(this.dir, 'public') + sep) !== 0
A
Arunoda Susiripala 已提交
1276 1277 1278 1279 1280 1281 1282 1283
    ) {
      // Seems like the user is trying to traverse the filesystem.
      return false
    }

    return true
  }

1284
  protected readBuildId(): string {
1285 1286 1287 1288 1289
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
1290
        throw new Error(
1291
          `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 已提交
1292
        )
1293 1294 1295
      }

      throw err
1296
    }
1297
  }
1298 1299 1300 1301

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