next-server.ts 35.0 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'
J
Joe Haddad 已提交
46
import { getSprCache, initializeSprCache, setSprCache } from './spr-cache'
47
import { isBlockedPage } from './utils'
48 49 50 51 52
import {
  Redirect,
  Rewrite,
  RouteType,
  Header,
53
  getRedirectStatus,
54
} from '../../lib/check-custom-routes'
55
import { normalizePagePath } from './normalize-page-path'
J
JJ Kasper 已提交
56 57

const getCustomRouteMatcher = pathMatch(true)
58 59 60

type NextConfig = any

61 62 63 64 65 66
type Middleware = (
  req: IncomingMessage,
  res: ServerResponse,
  next: (err?: Error) => void
) => void

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

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

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

133 134
    // Only serverRuntimeConfig needs the default
    // publicRuntimeConfig gets it's default in client/index.js
J
Joe Haddad 已提交
135 136 137 138 139
    const {
      serverRuntimeConfig = {},
      publicRuntimeConfig,
      assetPrefix,
      generateEtags,
140
      compress,
J
Joe Haddad 已提交
141
    } = this.nextConfig
142

T
Tim Neutkens 已提交
143
    this.buildId = this.readBuildId()
144

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

157 158
    // Only the `publicRuntimeConfig` key is exposed to the client side
    // It'll be rendered as part of __NEXT_DATA__ on the client side
159
    if (Object.keys(publicRuntimeConfig).length > 0) {
160
      this.renderOpts.runtimeConfig = publicRuntimeConfig
161 162
    }

163
    if (compress && this.nextConfig.target === 'server') {
164 165 166
      this.compression = compression() as Middleware
    }

167
    // Initialize next/config with the environment configuration
168 169 170 171
    envConfig.setConfig({
      serverRuntimeConfig,
      publicRuntimeConfig,
    })
172

173 174 175 176 177 178 179 180 181 182
    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 已提交
183
    this.router = new Router(this.generateRoutes())
184
    this.setAssetPrefix(assetPrefix)
J
JJ Kasper 已提交
185

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

J
JJ Kasper 已提交
198 199 200 201 202 203 204 205 206 207 208 209
    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 已提交
210
  }
N
nkzawa 已提交
211

212
  protected currentPhase(): string {
213
    return PHASE_PRODUCTION_SERVER
214 215
  }

216 217 218 219
  private logError(err: Error): void {
    if (this.onErrorMiddleware) {
      this.onErrorMiddleware({ err })
    }
220 221
    if (this.quiet) return
    // tslint:disable-next-line
222
    console.error(err)
223 224
  }

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

236 237 238
    // 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 已提交
239
    }
240

T
Tim Neutkens 已提交
241 242 243 244 245 246 247 248 249 250
    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, '')
    }

251
    res.statusCode = 200
252
    return this.run(req, res, parsedUrl).catch(err => {
J
Joe Haddad 已提交
253 254 255 256
      this.logError(err)
      res.statusCode = 500
      res.end('Internal Server Error')
    })
257 258
  }

259
  public getRequestHandler() {
260
    return this.handleRequest.bind(this)
N
nkzawa 已提交
261 262
  }

263
  public setAssetPrefix(prefix?: string) {
264
    this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
265 266
  }

267
  // Backwards compatibility
268
  public async prepare(): Promise<void> {}
N
nkzawa 已提交
269

T
Tim Neutkens 已提交
270
  // Backwards compatibility
271
  protected async close(): Promise<void> {}
T
Tim Neutkens 已提交
272

273
  protected setImmutableAssetCacheControl(res: ServerResponse) {
T
Tim Neutkens 已提交
274
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
N
nkzawa 已提交
275 276
  }

J
JJ Kasper 已提交
277 278 279 280
  protected getCustomRoutes() {
    return require(join(this.distDir, ROUTES_MANIFEST))
  }

281 282 283 284 285 286 287
  protected generateRoutes(): {
    routes: Route[]
    fsRoutes: Route[]
    catchAllRoute: Route
    pageChecker: PageChecker
    dynamicRoutes: DynamicRoutes | undefined
  } {
J
JJ Kasper 已提交
288 289
    this.customRoutes = this.getCustomRoutes()

290 291 292
    const publicRoutes = fs.existsSync(this.publicDir)
      ? this.generatePublicRoutes()
      : []
J
JJ Kasper 已提交
293

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

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

          // make sure to 404 for /_next/static itself
325 326 327 328 329 330
          if (!params.path) {
            await this.render404(req, res, parsedUrl)
            return {
              finished: true,
            }
          }
331

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

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

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

J
JJ Kasper 已提交
412
    if (this.customRoutes) {
413
      const { redirects, rewrites, headers } = this.customRoutes
J
JJ Kasper 已提交
414 415

      const getCustomRoute = (
416 417
        r: Rewrite | Redirect | Header,
        type: RouteType
J
JJ Kasper 已提交
418 419 420 421 422 423
      ) => ({
        ...r,
        type,
        matcher: getCustomRouteMatcher(r.source),
      })

424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442
      // 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 已提交
443 444 445 446 447 448
      const customRoutes = [
        ...redirects.map(r => getCustomRoute(r, 'redirect')),
        ...rewrites.map(r => getCustomRoute(r, 'rewrite')),
      ]

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

464 465 466 467 468 469 470 471 472 473 474
              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)]
                }
              })

475 476 477 478
              try {
                newUrl = destinationCompiler(params)
              } catch (err) {
                if (
J
Joe Haddad 已提交
479 480 481
                  err.message.match(
                    /Expected .*? to not repeat, but got an array/
                  )
482 483 484 485 486 487 488
                ) {
                  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
              }
489

490 491 492 493 494 495 496
              const parsedNewUrl = parseUrl(newUrl)
              const updatedDestination = formatUrl({
                ...parsedDestination,
                pathname: parsedNewUrl.pathname,
                hash: parsedNewUrl.hash,
                search: undefined,
              })
497

498
              if (route.type === 'redirect') {
499
                res.setHeader('Location', updatedDestination)
500
                res.statusCode = getRedirectStatus(route as Redirect)
501 502 503 504 505 506 507

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

508 509 510
                res.end()
                return {
                  finished: true,
J
JJ Kasper 已提交
511
                }
512
              } else {
513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532
                // 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 已提交
533 534
              }

535 536 537
              return {
                finished: false,
                pathname: newUrl,
538
                query: parsedDestination.query,
J
JJ Kasper 已提交
539 540 541 542 543 544 545
              }
            },
          } as Route
        })
      )
    }

546
    const catchPublicDirectoryRoute: Route = {
547
      match: route('/:path*'),
548
      type: 'route',
549
      name: 'Catch public directory route',
550
      fn: async (req, res, params, parsedUrl) => {
551
        const { pathname } = parsedUrl
552 553 554 555 556 557 558 559 560 561 562
        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,
          }
        }

563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580
        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')
        }

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

        await this.render(req, res, pathname, query, parsedUrl)
594 595 596 597
        return {
          finished: true,
        }
      },
598
    }
599

600
    if (this.nextConfig.useFileSystemPublicRoutes) {
J
Joe Haddad 已提交
601
      this.dynamicRoutes = this.getDynamicRoutes()
J
Joe Haddad 已提交
602

603 604 605
      // 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.
606
      // See more: https://github.com/zeit/next.js/issues/2617
607
      routes.push(catchAllRoute)
608
    }
N
nkzawa 已提交
609

610 611 612 613 614 615 616
    return {
      routes,
      fsRoutes,
      catchAllRoute,
      dynamicRoutes: this.dynamicRoutes,
      pageChecker: this.hasPage.bind(this),
    }
T
Tim Neutkens 已提交
617 618
  }

619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636
  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
  }

637 638 639 640 641 642 643 644 645
  protected async _beforeCatchAllRender(
    _req: IncomingMessage,
    _res: ServerResponse,
    _params: Params,
    _parsedUrl: UrlWithParsedQuery
  ) {
    return false
  }

646 647 648
  // Used to build API page in development
  protected async ensureApiPage(pathname: string) {}

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

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

676
    if (!pageFound) {
677
      return false
J
JJ Kasper 已提交
678
    }
679 680 681 682 683 684
    // 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)
685
    query = { ...query, ...params }
J
JJ Kasper 已提交
686

687
    if (!this.renderOpts.dev && this._isLikeServerless) {
688
      if (typeof pageModule.default === 'function') {
689
        this.prepareServerlessUrl(req, query)
690 691
        await pageModule.default(req, res)
        return true
J
JJ Kasper 已提交
692 693 694
      }
    }

695
    await apiResolver(req, res, query, pageModule, this.onErrorMiddleware)
696
    return true
L
Lukáš Huvar 已提交
697 698
  }

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

731
  protected getDynamicRoutes() {
732 733 734
    const dynamicRoutedPages = Object.keys(this.pagesManifest!).filter(
      isDynamicRoute
    )
735 736 737 738
    return getSortedRoutes(dynamicRoutedPages).map(page => ({
      page,
      match: getRouteMatcher(getRouteRegex(page)),
    }))
J
Joe Haddad 已提交
739 740
  }

741 742 743 744 745 746
  private handleCompression(req: IncomingMessage, res: ServerResponse) {
    if (this.compression) {
      this.compression(req, res, () => {})
    }
  }

747
  protected async run(
J
Joe Haddad 已提交
748 749
    req: IncomingMessage,
    res: ServerResponse,
750
    parsedUrl: UrlWithParsedQuery
J
Joe Haddad 已提交
751
  ) {
752 753
    this.handleCompression(req, res)

754
    try {
755 756
      const matched = await this.router.execute(req, res, parsedUrl)
      if (matched) {
757 758 759 760 761 762 763 764
        return
      }
    } catch (err) {
      if (err.code === 'DECODE_FAILED') {
        res.statusCode = 400
        return this.renderError(null, req, res, '/_error', {})
      }
      throw err
765 766
    }

767
    await this.render404(req, res, parsedUrl)
N
nkzawa 已提交
768 769
  }

770
  protected async sendHTML(
J
Joe Haddad 已提交
771 772
    req: IncomingMessage,
    res: ServerResponse,
773
    html: string
J
Joe Haddad 已提交
774
  ) {
T
Tim Neutkens 已提交
775 776
    const { generateEtags, poweredByHeader } = this.renderOpts
    return sendHTML(req, res, html, { generateEtags, poweredByHeader })
777 778
  }

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

    if (
      url.match(/^\/_next\//) ||
      (this.hasStaticDir && url.match(/^\/static\//))
    ) {
792 793 794
      return this.handleRequest(req, res, parsedUrl)
    }

795
    if (isBlockedPage(pathname)) {
796
      return this.render404(req, res, parsedUrl)
797 798
    }

T
Tim Neutkens 已提交
799
    const html = await this.renderToHTML(req, res, pathname, query, {})
800 801
    // Request was ended by the user
    if (html === null) {
802 803 804
      return
    }

805
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
806
  }
N
nkzawa 已提交
807

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

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

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

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

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

936 937 938 939 940 941 942 943 944 945
      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 已提交
946 947 948 949 950 951 952
      return renderToHTML(req, res, pathname, query, {
        ...result,
        ...opts,
      })
    }

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

    // Complete the response with cached data if its present
956
    const cachedData = await getSprCache(ssgCacheKey)
J
JJ Kasper 已提交
957
    if (cachedData) {
958
      const data = isDataReq
J
JJ Kasper 已提交
959 960 961 962 963 964
        ? JSON.stringify(cachedData.pageData)
        : cachedData.html

      this.__sendPayload(
        res,
        data,
965
        isDataReq ? 'application/json' : 'text/html; charset=utf-8',
J
JJ Kasper 已提交
966
        cachedData.curRevalidate
J
JJ Kasper 已提交
967 968 969 970 971 972
      )

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

J
JJ Kasper 已提交
975 976 977 978
    // If we're here, that means data is missing or it's stale.

    const doRender = withCoalescedInvoke(async function(): Promise<{
      html: string | null
979
      pageData: any
J
JJ Kasper 已提交
980 981
      sprRevalidate: number | false
    }> {
982
      let pageData: any
J
JJ Kasper 已提交
983 984 985 986 987 988
      let html: string | null
      let sprRevalidate: number | false

      let renderResult
      // handle serverless
      if (isLikeServerless) {
989 990 991 992 993
        renderResult = await (result.Component as any).renderReqToHTML(
          req,
          res,
          true
        )
J
JJ Kasper 已提交
994 995

        html = renderResult.html
996
        pageData = renderResult.renderOpts.pageData
J
JJ Kasper 已提交
997 998 999 1000 1001 1002 1003 1004 1005
        sprRevalidate = renderResult.renderOpts.revalidate
      } else {
        const renderOpts = {
          ...result,
          ...opts,
        }
        renderResult = await renderToHTML(req, res, pathname, query, renderOpts)

        html = renderResult
1006
        pageData = renderOpts.pageData
J
JJ Kasper 已提交
1007 1008 1009
        sprRevalidate = renderOpts.revalidate
      }

1010
      return { html, pageData, sprRevalidate }
1011
    })
J
JJ Kasper 已提交
1012

1013 1014
    return doRender(ssgCacheKey, []).then(
      async ({ isOrigin, value: { html, pageData, sprRevalidate } }) => {
J
JJ Kasper 已提交
1015 1016 1017 1018
        // Respond to the request if a payload wasn't sent above (from cache)
        if (!isResSent(res)) {
          this.__sendPayload(
            res,
1019 1020
            isDataReq ? JSON.stringify(pageData) : html,
            isDataReq ? 'application/json' : 'text/html; charset=utf-8',
J
JJ Kasper 已提交
1021
            sprRevalidate
J
JJ Kasper 已提交
1022 1023 1024 1025 1026 1027
          )
        }

        // Update the SPR cache if the head request
        if (isOrigin) {
          await setSprCache(
1028 1029
            ssgCacheKey,
            { html: html!, pageData },
J
JJ Kasper 已提交
1030 1031 1032 1033 1034 1035 1036
            sprRevalidate
          )
        }

        return null
      }
    )
1037 1038
  }

J
Joe Haddad 已提交
1039
  public renderToHTML(
J
Joe Haddad 已提交
1040 1041 1042 1043
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
J
Joe Haddad 已提交
1044 1045 1046 1047 1048 1049
    {
      amphtml,
      hasAmp,
    }: {
      amphtml?: boolean
      hasAmp?: boolean
1050
    } = {}
J
Joe Haddad 已提交
1051
  ): Promise<string | null> {
J
Joe Haddad 已提交
1052 1053
    return this.findPageComponents(pathname, query)
      .then(
1054
        result => {
J
Joe Haddad 已提交
1055 1056 1057 1058
          return this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
1059
            result.unstable_getStaticProps
1060
              ? { _nextDataReq: query._nextDataReq }
1061
              : query,
J
Joe Haddad 已提交
1062
            result,
T
Tim Neutkens 已提交
1063
            { ...this.renderOpts, amphtml, hasAmp }
J
Joe Haddad 已提交
1064 1065
          )
        },
1066
        err => {
J
Joe Haddad 已提交
1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077
          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(
1078 1079
              result => {
                return this.renderToHTMLWithComponents(
J
Joe Haddad 已提交
1080 1081 1082
                  req,
                  res,
                  dynamicRoute.page,
J
JJ Kasper 已提交
1083 1084 1085
                  // only add params for SPR enabled pages
                  {
                    ...(result.unstable_getStaticProps
1086
                      ? { _nextDataReq: query._nextDataReq }
J
JJ Kasper 已提交
1087 1088 1089
                      : query),
                    ...params,
                  },
J
Joe Haddad 已提交
1090
                  result,
J
JJ Kasper 已提交
1091 1092
                  {
                    ...this.renderOpts,
1093
                    params,
J
JJ Kasper 已提交
1094 1095 1096
                    amphtml,
                    hasAmp,
                  }
1097
                )
1098
              }
J
Joe Haddad 已提交
1099 1100 1101 1102
            )
          }

          return Promise.reject(err)
1103
        }
J
Joe Haddad 已提交
1104
      )
1105
      .catch(err => {
J
Joe Haddad 已提交
1106 1107 1108 1109 1110 1111 1112 1113 1114
        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 已提交
1115 1116
  }

J
Joe Haddad 已提交
1117 1118 1119 1120 1121
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1122
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1123 1124 1125
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
1126
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
1127
    )
N
Naoyuki Kanezawa 已提交
1128
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
1129
    if (html === null) {
1130 1131
      return
    }
1132
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
1133 1134
  }

J
Joe Haddad 已提交
1135 1136 1137 1138 1139
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
1140
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1141
  ) {
1142 1143
    let result: null | LoadComponentsReturnType = null

1144 1145 1146 1147
    const { static404, pages404 } = this.nextConfig.experimental
    const is404 = res.statusCode === 404
    let using404Page = false

1148
    // use static 404 page if available and is 404 response
1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169
    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
          }
1170 1171 1172 1173 1174 1175 1176 1177
        }
      }
    }

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

1178 1179 1180 1181 1182
    let html
    try {
      html = await this.renderToHTMLWithComponents(
        req,
        res,
1183
        using404Page ? '/404' : '/_error',
1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196
        query,
        result,
        {
          ...this.renderOpts,
          err,
        }
      )
    } catch (err) {
      console.error(err)
      res.statusCode = 500
      html = 'Internal Server Error'
    }
    return html
N
Naoyuki Kanezawa 已提交
1197 1198
  }

J
Joe Haddad 已提交
1199 1200 1201
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
1202
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1203
  ): Promise<void> {
1204 1205
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
1206
    if (!pathname) {
1207 1208
      throw new Error('pathname is undefined')
    }
N
Naoyuki Kanezawa 已提交
1209
    res.statusCode = 404
1210
    return this.renderError(null, req, res, pathname, query)
N
Naoyuki Kanezawa 已提交
1211
  }
N
Naoyuki Kanezawa 已提交
1212

J
Joe Haddad 已提交
1213 1214 1215 1216
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
1217
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1218
  ): Promise<void> {
A
Arunoda Susiripala 已提交
1219
    if (!this.isServeableUrl(path)) {
1220
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
1221 1222
    }

1223 1224 1225 1226 1227 1228
    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 已提交
1229
    try {
1230
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
1231
    } catch (err) {
T
Tim Neutkens 已提交
1232
      if (err.code === 'ENOENT' || err.statusCode === 404) {
1233
        this.render404(req, res, parsedUrl)
1234 1235 1236
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
1237 1238 1239 1240 1241 1242
      } else {
        throw err
      }
    }
  }

1243
  private isServeableUrl(path: string): boolean {
A
Arunoda Susiripala 已提交
1244 1245
    const resolved = resolve(path)
    if (
1246
      resolved.indexOf(join(this.distDir) + sep) !== 0 &&
1247 1248
      resolved.indexOf(join(this.dir, 'static') + sep) !== 0 &&
      resolved.indexOf(join(this.dir, 'public') + sep) !== 0
A
Arunoda Susiripala 已提交
1249 1250 1251 1252 1253 1254 1255 1256
    ) {
      // Seems like the user is trying to traverse the filesystem.
      return false
    }

    return true
  }

1257
  protected readBuildId(): string {
1258 1259 1260 1261 1262
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
1263
        throw new Error(
1264
          `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 已提交
1265
        )
1266 1267 1268
      }

      throw err
1269
    }
1270
  }
1271 1272 1273 1274

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