next-server.ts 31.9 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
import { join, resolve, sep } from 'path'
J
Joe Haddad 已提交
5
import { compile as compilePathToRegex } from 'path-to-regexp'
6
import { parse as parseQs, ParsedUrlQuery } from 'querystring'
7
import { format as formatUrl, parse as parseUrl, UrlWithParsedQuery } from 'url'
J
Joe Haddad 已提交
8

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

const getCustomRouteMatcher = pathMatch(true)
56 57 58

type NextConfig = any

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

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

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

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

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

T
Tim Neutkens 已提交
140
    this.buildId = this.readBuildId()
141

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

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

159
    if (compress && this.nextConfig.target === 'server') {
160 161 162
      this.compression = compression() as Middleware
    }

163
    // Initialize next/config with the environment configuration
164 165 166 167 168 169
    if (this.nextConfig.target === 'server') {
      envConfig.setConfig({
        serverRuntimeConfig,
        publicRuntimeConfig,
      })
    }
170

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

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

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

210
  protected currentPhase(): string {
211
    return PHASE_PRODUCTION_SERVER
212 213
  }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

J
JJ Kasper 已提交
408
    if (this.customRoutes) {
409
      const { redirects, rewrites, headers } = this.customRoutes
J
JJ Kasper 已提交
410 411

      const getCustomRoute = (
412 413
        r: Rewrite | Redirect | Header,
        type: RouteType
J
JJ Kasper 已提交
414 415 416 417 418 419
      ) => ({
        ...r,
        type,
        matcher: getCustomRouteMatcher(r.source),
      })

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

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

460 461 462 463 464 465 466 467 468 469 470
              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)]
                }
              })

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

              if (route.type === 'redirect') {
487
                const parsedNewUrl = parseUrl(newUrl)
488 489 490 491 492 493 494 495
                const updatedDestination = formatUrl({
                  ...parsedDestination,
                  pathname: parsedNewUrl.pathname,
                  hash: parsedNewUrl.hash,
                  search: undefined,
                })

                res.setHeader('Location', updatedDestination)
496
                res.statusCode = getRedirectStatus(route as Redirect)
497 498 499 500 501 502 503

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

504 505 506
                res.end()
                return {
                  finished: true,
J
JJ Kasper 已提交
507
                }
508 509
              } else {
                ;(_req as any)._nextDidRewrite = true
J
JJ Kasper 已提交
510 511
              }

512 513 514
              return {
                finished: false,
                pathname: newUrl,
515
                query: parsedDestination.query,
J
JJ Kasper 已提交
516 517 518 519 520 521 522
              }
            },
          } as Route
        })
      )
    }

523 524
    const catchAllRoute: Route = {
      match: route('/:path*'),
525
      type: 'route',
526
      name: 'Catchall render',
527
      fn: async (req, res, params, parsedUrl) => {
528 529 530 531 532 533 534 535 536 537 538 539
        const { pathname, query } = parsedUrl
        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,
          }
        }

540
        if (params?.path?.[0] === 'api') {
541 542 543 544 545 546 547 548 549 550 551
          const handled = await this.handleApiRequest(
            req as NextApiRequest,
            res as NextApiResponse,
            pathname!
          )
          if (handled) {
            return { finished: true }
          }
        }

        await this.render(req, res, pathname, query, parsedUrl)
552 553 554 555
        return {
          finished: true,
        }
      },
556
    }
557

558
    if (this.nextConfig.useFileSystemPublicRoutes) {
J
Joe Haddad 已提交
559
      this.dynamicRoutes = this.getDynamicRoutes()
J
Joe Haddad 已提交
560

561 562 563
      // 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.
564
      // See more: https://github.com/zeit/next.js/issues/2617
565
      routes.push(catchAllRoute)
566
    }
N
nkzawa 已提交
567

568 569 570 571 572 573 574
    return {
      routes,
      fsRoutes,
      catchAllRoute,
      dynamicRoutes: this.dynamicRoutes,
      pageChecker: this.hasPage.bind(this),
    }
T
Tim Neutkens 已提交
575 576
  }

577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594
  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
  }

595 596 597 598 599 600 601 602 603
  protected async _beforeCatchAllRender(
    _req: IncomingMessage,
    _res: ServerResponse,
    _params: Params,
    _parsedUrl: UrlWithParsedQuery
  ) {
    return false
  }

604 605 606
  // Used to build API page in development
  protected async ensureApiPage(pathname: string) {}

L
Lukáš Huvar 已提交
607 608 609 610 611 612
  /**
   * Resolves `API` request, in development builds on demand
   * @param req http request
   * @param res http response
   * @param pathname path of request
   */
J
Joe Haddad 已提交
613
  private async handleApiRequest(
614 615
    req: IncomingMessage,
    res: ServerResponse,
616
    pathname: string
J
Joe Haddad 已提交
617
  ) {
618
    let page = pathname
L
Lukáš Huvar 已提交
619
    let params: Params | boolean = false
620
    let pageFound = await this.hasPage(page)
J
JJ Kasper 已提交
621

622
    if (!pageFound && this.dynamicRoutes) {
L
Lukáš Huvar 已提交
623 624 625
      for (const dynamicRoute of this.dynamicRoutes) {
        params = dynamicRoute.match(pathname)
        if (params) {
626 627
          page = dynamicRoute.page
          pageFound = true
L
Lukáš Huvar 已提交
628 629 630 631 632
          break
        }
      }
    }

633
    if (!pageFound) {
634
      return false
J
JJ Kasper 已提交
635
    }
636 637 638 639 640 641
    // 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)
J
JJ Kasper 已提交
642

643
    if (!this.renderOpts.dev && this._isLikeServerless) {
644
      if (typeof pageModule.default === 'function') {
645 646
        await pageModule.default(req, res)
        return true
J
JJ Kasper 已提交
647 648 649
      }
    }

650
    await apiResolver(req, res, params, pageModule, this.onErrorMiddleware)
651
    return true
L
Lukáš Huvar 已提交
652 653
  }

654
  protected generatePublicRoutes(): Route[] {
655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673
    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
            )
674 675 676
            return {
              finished: true,
            }
677 678 679 680 681 682 683
          }
          return {
            finished: false,
          }
        },
      } as Route,
    ]
684 685
  }

686
  protected getDynamicRoutes() {
687 688 689
    const dynamicRoutedPages = Object.keys(this.pagesManifest!).filter(
      isDynamicRoute
    )
690 691 692 693
    return getSortedRoutes(dynamicRoutedPages).map(page => ({
      page,
      match: getRouteMatcher(getRouteRegex(page)),
    }))
J
Joe Haddad 已提交
694 695
  }

696 697 698 699 700 701
  private handleCompression(req: IncomingMessage, res: ServerResponse) {
    if (this.compression) {
      this.compression(req, res, () => {})
    }
  }

702
  protected async run(
J
Joe Haddad 已提交
703 704
    req: IncomingMessage,
    res: ServerResponse,
705
    parsedUrl: UrlWithParsedQuery
J
Joe Haddad 已提交
706
  ) {
707 708
    this.handleCompression(req, res)

709
    try {
710 711
      const matched = await this.router.execute(req, res, parsedUrl)
      if (matched) {
712 713 714 715 716 717 718 719
        return
      }
    } catch (err) {
      if (err.code === 'DECODE_FAILED') {
        res.statusCode = 400
        return this.renderError(null, req, res, '/_error', {})
      }
      throw err
720 721
    }

722
    await this.render404(req, res, parsedUrl)
N
nkzawa 已提交
723 724
  }

725
  protected async sendHTML(
J
Joe Haddad 已提交
726 727
    req: IncomingMessage,
    res: ServerResponse,
728
    html: string
J
Joe Haddad 已提交
729
  ) {
T
Tim Neutkens 已提交
730 731
    const { generateEtags, poweredByHeader } = this.renderOpts
    return sendHTML(req, res, html, { generateEtags, poweredByHeader })
732 733
  }

J
Joe Haddad 已提交
734 735 736 737 738
  public async render(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
739
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
740
  ): Promise<void> {
741
    const url: any = req.url
742 743 744 745 746

    if (
      url.match(/^\/_next\//) ||
      (this.hasStaticDir && url.match(/^\/static\//))
    ) {
747 748 749
      return this.handleRequest(req, res, parsedUrl)
    }

750
    if (isBlockedPage(pathname)) {
751
      return this.render404(req, res, parsedUrl)
752 753
    }

T
Tim Neutkens 已提交
754
    const html = await this.renderToHTML(req, res, pathname, query, {})
755 756
    // Request was ended by the user
    if (html === null) {
757 758 759
      return
    }

760
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
761
  }
N
nkzawa 已提交
762

J
Joe Haddad 已提交
763
  private async findPageComponents(
J
Joe Haddad 已提交
764
    pathname: string,
765
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
766
  ) {
767
    const serverless = !this.renderOpts.dev && this._isLikeServerless
J
JJ Kasper 已提交
768 769 770
    // try serving a static AMP version first
    if (query.amp) {
      try {
J
Joe Haddad 已提交
771 772 773 774
        return await loadComponents(
          this.distDir,
          this.buildId,
          (pathname === '/' ? '/index' : pathname) + '.amp',
775
          serverless
J
Joe Haddad 已提交
776
        )
J
JJ Kasper 已提交
777 778 779 780
      } catch (err) {
        if (err.code !== 'ENOENT') throw err
      }
    }
J
Joe Haddad 已提交
781 782 783 784
    return await loadComponents(
      this.distDir,
      this.buildId,
      pathname,
785
      serverless
J
Joe Haddad 已提交
786 787 788
    )
  }

J
JJ Kasper 已提交
789 790 791 792 793 794
  private __sendPayload(
    res: ServerResponse,
    payload: any,
    type: string,
    revalidate?: number | false
  ) {
J
JJ Kasper 已提交
795
    // TODO: ETag? Cache-Control headers? Next-specific headers?
J
JJ Kasper 已提交
796
    res.setHeader('Content-Type', type)
J
JJ Kasper 已提交
797
    res.setHeader('Content-Length', Buffer.byteLength(payload))
J
Joe Haddad 已提交
798 799 800 801 802 803 804 805 806 807 808 809
    if (!this.renderOpts.dev) {
      if (revalidate) {
        res.setHeader(
          'Cache-Control',
          `s-maxage=${revalidate}, stale-while-revalidate`
        )
      } else if (revalidate === false) {
        res.setHeader(
          'Cache-Control',
          `s-maxage=31536000, stale-while-revalidate`
        )
      }
J
JJ Kasper 已提交
810
    }
J
JJ Kasper 已提交
811 812 813
    res.end(payload)
  }

J
Joe Haddad 已提交
814 815 816 817 818 819
  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
    result: LoadComponentsReturnType,
820
    opts: any
J
JJ Kasper 已提交
821
  ): Promise<string | null> {
J
JJ Kasper 已提交
822
    // handle static page
J
Joe Haddad 已提交
823 824 825 826
    if (typeof result.Component === 'string') {
      return result.Component
    }

J
JJ Kasper 已提交
827 828
    // check request state
    const isLikeServerless =
J
Joe Haddad 已提交
829
      typeof result.Component === 'object' &&
J
JJ Kasper 已提交
830
      typeof result.Component.renderReqToHTML === 'function'
831
    const isSSG = !!result.unstable_getStaticProps
J
JJ Kasper 已提交
832 833

    // non-spr requests should render like normal
834
    if (!isSSG) {
J
JJ Kasper 已提交
835 836
      // handle serverless
      if (isLikeServerless) {
837 838 839
        const curUrl = parseUrl(req.url!, true)
        req.url = formatUrl({
          ...curUrl,
840
          search: undefined,
841 842 843 844 845
          query: {
            ...curUrl.query,
            ...query,
          },
        })
J
JJ Kasper 已提交
846 847 848 849 850 851 852 853 854 855
        return result.Component.renderReqToHTML(req, res)
      }

      return renderToHTML(req, res, pathname, query, {
        ...result,
        ...opts,
      })
    }

    // Toggle whether or not this is an SPR Data request
856 857
    const isDataReq = query._nextDataReq
    delete query._nextDataReq
858

J
JJ Kasper 已提交
859
    // Compute the SPR cache key
860
    const ssgCacheKey = parseUrl(req.url || '').pathname!
J
JJ Kasper 已提交
861 862

    // Complete the response with cached data if its present
863
    const cachedData = await getSprCache(ssgCacheKey)
J
JJ Kasper 已提交
864
    if (cachedData) {
865
      const data = isDataReq
J
JJ Kasper 已提交
866 867 868 869 870 871
        ? JSON.stringify(cachedData.pageData)
        : cachedData.html

      this.__sendPayload(
        res,
        data,
872
        isDataReq ? 'application/json' : 'text/html; charset=utf-8',
J
JJ Kasper 已提交
873
        cachedData.curRevalidate
J
JJ Kasper 已提交
874 875 876 877 878 879
      )

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

J
JJ Kasper 已提交
882 883 884 885
    // If we're here, that means data is missing or it's stale.

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

    const doRender = withCoalescedInvoke(async function(): Promise<{
      html: string | null
894
      pageData: any
J
JJ Kasper 已提交
895 896
      sprRevalidate: number | false
    }> {
897
      let pageData: any
J
JJ Kasper 已提交
898 899 900 901 902 903 904 905 906
      let html: string | null
      let sprRevalidate: number | false

      let renderResult
      // handle serverless
      if (isLikeServerless) {
        renderResult = await result.Component.renderReqToHTML(req, res, true)

        html = renderResult.html
907
        pageData = renderResult.renderOpts.pageData
J
JJ Kasper 已提交
908 909 910 911 912 913 914 915 916
        sprRevalidate = renderResult.renderOpts.revalidate
      } else {
        const renderOpts = {
          ...result,
          ...opts,
        }
        renderResult = await renderToHTML(req, res, pathname, query, renderOpts)

        html = renderResult
917
        pageData = renderOpts.pageData
J
JJ Kasper 已提交
918 919 920
        sprRevalidate = renderOpts.revalidate
      }

921
      return { html, pageData, sprRevalidate }
922
    })
J
JJ Kasper 已提交
923

924 925
    return doRender(ssgCacheKey, []).then(
      async ({ isOrigin, value: { html, pageData, sprRevalidate } }) => {
J
JJ Kasper 已提交
926 927 928 929
        // Respond to the request if a payload wasn't sent above (from cache)
        if (!isResSent(res)) {
          this.__sendPayload(
            res,
930 931
            isDataReq ? JSON.stringify(pageData) : html,
            isDataReq ? 'application/json' : 'text/html; charset=utf-8',
J
JJ Kasper 已提交
932
            sprRevalidate
J
JJ Kasper 已提交
933 934 935 936 937 938
          )
        }

        // Update the SPR cache if the head request
        if (isOrigin) {
          await setSprCache(
939 940
            ssgCacheKey,
            { html: html!, pageData },
J
JJ Kasper 已提交
941 942 943 944 945 946 947
            sprRevalidate
          )
        }

        return null
      }
    )
948 949
  }

J
Joe Haddad 已提交
950
  public renderToHTML(
J
Joe Haddad 已提交
951 952 953 954
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
J
Joe Haddad 已提交
955 956 957 958 959 960
    {
      amphtml,
      hasAmp,
    }: {
      amphtml?: boolean
      hasAmp?: boolean
961
    } = {}
J
Joe Haddad 已提交
962
  ): Promise<string | null> {
J
Joe Haddad 已提交
963 964
    return this.findPageComponents(pathname, query)
      .then(
965
        result => {
J
Joe Haddad 已提交
966 967 968 969
          return this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
970
            result.unstable_getStaticProps
971
              ? { _nextDataReq: query._nextDataReq }
972
              : query,
J
Joe Haddad 已提交
973
            result,
T
Tim Neutkens 已提交
974
            { ...this.renderOpts, amphtml, hasAmp }
J
Joe Haddad 已提交
975 976
          )
        },
977
        err => {
J
Joe Haddad 已提交
978 979 980 981 982 983 984 985 986 987 988
          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(
989 990
              result => {
                return this.renderToHTMLWithComponents(
J
Joe Haddad 已提交
991 992 993
                  req,
                  res,
                  dynamicRoute.page,
J
JJ Kasper 已提交
994 995 996
                  // only add params for SPR enabled pages
                  {
                    ...(result.unstable_getStaticProps
997
                      ? { _nextDataReq: query._nextDataReq }
J
JJ Kasper 已提交
998 999 1000
                      : query),
                    ...params,
                  },
J
Joe Haddad 已提交
1001
                  result,
J
JJ Kasper 已提交
1002 1003 1004 1005 1006
                  {
                    ...this.renderOpts,
                    amphtml,
                    hasAmp,
                  }
1007
                )
1008
              }
J
Joe Haddad 已提交
1009 1010 1011 1012
            )
          }

          return Promise.reject(err)
1013
        }
J
Joe Haddad 已提交
1014
      )
1015
      .catch(err => {
J
Joe Haddad 已提交
1016 1017 1018 1019 1020 1021 1022 1023 1024
        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 已提交
1025 1026
  }

J
Joe Haddad 已提交
1027 1028 1029 1030 1031
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1032
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1033 1034 1035
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
1036
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
1037
    )
N
Naoyuki Kanezawa 已提交
1038
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
1039
    if (html === null) {
1040 1041
      return
    }
1042
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
1043 1044
  }

J
Joe Haddad 已提交
1045 1046 1047 1048 1049
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
1050
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1051
  ) {
1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068
    let result: null | LoadComponentsReturnType = null

    // use static 404 page if available and is 404 response
    if (this.nextConfig.experimental.static404 && err === null) {
      try {
        result = await this.findPageComponents('/_errors/404')
      } catch (err) {
        if (err.code !== 'ENOENT') {
          throw err
        }
      }
    }

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

1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087
    let html
    try {
      html = await this.renderToHTMLWithComponents(
        req,
        res,
        '/_error',
        query,
        result,
        {
          ...this.renderOpts,
          err,
        }
      )
    } catch (err) {
      console.error(err)
      res.statusCode = 500
      html = 'Internal Server Error'
    }
    return html
N
Naoyuki Kanezawa 已提交
1088 1089
  }

J
Joe Haddad 已提交
1090 1091 1092
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
1093
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1094
  ): Promise<void> {
1095 1096
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
1097
    if (!pathname) {
1098 1099
      throw new Error('pathname is undefined')
    }
N
Naoyuki Kanezawa 已提交
1100
    res.statusCode = 404
1101
    return this.renderError(null, req, res, pathname, query)
N
Naoyuki Kanezawa 已提交
1102
  }
N
Naoyuki Kanezawa 已提交
1103

J
Joe Haddad 已提交
1104 1105 1106 1107
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
1108
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1109
  ): Promise<void> {
A
Arunoda Susiripala 已提交
1110
    if (!this.isServeableUrl(path)) {
1111
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
1112 1113
    }

1114 1115 1116 1117 1118 1119
    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 已提交
1120
    try {
1121
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
1122
    } catch (err) {
T
Tim Neutkens 已提交
1123
      if (err.code === 'ENOENT' || err.statusCode === 404) {
1124
        this.render404(req, res, parsedUrl)
1125 1126 1127
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
1128 1129 1130 1131 1132 1133
      } else {
        throw err
      }
    }
  }

1134
  private isServeableUrl(path: string): boolean {
A
Arunoda Susiripala 已提交
1135 1136
    const resolved = resolve(path)
    if (
1137
      resolved.indexOf(join(this.distDir) + sep) !== 0 &&
1138 1139
      resolved.indexOf(join(this.dir, 'static') + sep) !== 0 &&
      resolved.indexOf(join(this.dir, 'public') + sep) !== 0
A
Arunoda Susiripala 已提交
1140 1141 1142 1143 1144 1145 1146 1147
    ) {
      // Seems like the user is trying to traverse the filesystem.
      return false
    }

    return true
  }

1148
  protected readBuildId(): string {
1149 1150 1151 1152 1153
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
1154
        throw new Error(
1155
          `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 已提交
1156
        )
1157 1158 1159
      }

      throw err
1160
    }
1161
  }
1162 1163 1164 1165

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