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

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

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

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

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

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

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

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

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

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

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

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

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

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

525
    const catchPublicDirectoryRoute: Route = {
526
      match: route('/:path*'),
527
      type: 'route',
528
      name: 'Catch public directory route',
529
      fn: async (req, res, params, parsedUrl) => {
530
        const { pathname } = parsedUrl
531 532 533 534 535 536 537 538 539 540 541
        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,
          }
        }

542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559
        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')
        }

560
        if (params?.path?.[0] === 'api') {
561 562 563 564 565 566 567 568 569 570 571
          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)
572 573 574 575
        return {
          finished: true,
        }
      },
576
    }
577

578
    if (this.nextConfig.useFileSystemPublicRoutes) {
J
Joe Haddad 已提交
579
      this.dynamicRoutes = this.getDynamicRoutes()
J
Joe Haddad 已提交
580

581 582 583
      // 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.
584
      // See more: https://github.com/zeit/next.js/issues/2617
585
      routes.push(catchAllRoute)
586
    }
N
nkzawa 已提交
587

588 589 590 591 592 593 594
    return {
      routes,
      fsRoutes,
      catchAllRoute,
      dynamicRoutes: this.dynamicRoutes,
      pageChecker: this.hasPage.bind(this),
    }
T
Tim Neutkens 已提交
595 596
  }

597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614
  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
  }

615 616 617 618 619 620 621 622 623
  protected async _beforeCatchAllRender(
    _req: IncomingMessage,
    _res: ServerResponse,
    _params: Params,
    _parsedUrl: UrlWithParsedQuery
  ) {
    return false
  }

624 625 626
  // Used to build API page in development
  protected async ensureApiPage(pathname: string) {}

L
Lukáš Huvar 已提交
627 628 629 630 631 632
  /**
   * Resolves `API` request, in development builds on demand
   * @param req http request
   * @param res http response
   * @param pathname path of request
   */
J
Joe Haddad 已提交
633
  private async handleApiRequest(
634 635
    req: IncomingMessage,
    res: ServerResponse,
636
    pathname: string
J
Joe Haddad 已提交
637
  ) {
638
    let page = pathname
L
Lukáš Huvar 已提交
639
    let params: Params | boolean = false
640
    let pageFound = await this.hasPage(page)
J
JJ Kasper 已提交
641

642
    if (!pageFound && this.dynamicRoutes) {
L
Lukáš Huvar 已提交
643 644 645
      for (const dynamicRoute of this.dynamicRoutes) {
        params = dynamicRoute.match(pathname)
        if (params) {
646 647
          page = dynamicRoute.page
          pageFound = true
L
Lukáš Huvar 已提交
648 649 650 651 652
          break
        }
      }
    }

653
    if (!pageFound) {
654
      return false
J
JJ Kasper 已提交
655
    }
656 657 658 659 660 661
    // 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 已提交
662

663
    if (!this.renderOpts.dev && this._isLikeServerless) {
664
      if (typeof pageModule.default === 'function') {
665 666
        await pageModule.default(req, res)
        return true
J
JJ Kasper 已提交
667 668 669
      }
    }

670
    await apiResolver(req, res, params, pageModule, this.onErrorMiddleware)
671
    return true
L
Lukáš Huvar 已提交
672 673
  }

674
  protected generatePublicRoutes(): Route[] {
675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693
    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
            )
694 695 696
            return {
              finished: true,
            }
697 698 699 700 701 702 703
          }
          return {
            finished: false,
          }
        },
      } as Route,
    ]
704 705
  }

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

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

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

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

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

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

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

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

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

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

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

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

J
JJ Kasper 已提交
809 810 811 812 813 814
  private __sendPayload(
    res: ServerResponse,
    payload: any,
    type: string,
    revalidate?: number | false
  ) {
J
JJ Kasper 已提交
815
    // TODO: ETag? Cache-Control headers? Next-specific headers?
J
JJ Kasper 已提交
816
    res.setHeader('Content-Type', type)
J
JJ Kasper 已提交
817
    res.setHeader('Content-Length', Buffer.byteLength(payload))
J
Joe Haddad 已提交
818 819 820 821 822 823 824 825 826 827 828 829
    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 已提交
830
    }
J
JJ Kasper 已提交
831 832 833
    res.end(payload)
  }

J
Joe Haddad 已提交
834 835 836 837 838 839
  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
    result: LoadComponentsReturnType,
840
    opts: any
J
JJ Kasper 已提交
841
  ): Promise<string | null> {
J
JJ Kasper 已提交
842
    // handle static page
J
Joe Haddad 已提交
843 844 845 846
    if (typeof result.Component === 'string') {
      return result.Component
    }

J
JJ Kasper 已提交
847 848
    // check request state
    const isLikeServerless =
J
Joe Haddad 已提交
849
      typeof result.Component === 'object' &&
J
JJ Kasper 已提交
850
      typeof result.Component.renderReqToHTML === 'function'
851
    const isSSG = !!result.unstable_getStaticProps
J
JJ Kasper 已提交
852 853

    // non-spr requests should render like normal
854
    if (!isSSG) {
J
JJ Kasper 已提交
855 856
      // handle serverless
      if (isLikeServerless) {
857 858 859
        const curUrl = parseUrl(req.url!, true)
        req.url = formatUrl({
          ...curUrl,
860
          search: undefined,
861 862 863 864 865
          query: {
            ...curUrl.query,
            ...query,
          },
        })
J
JJ Kasper 已提交
866 867 868 869 870 871 872 873 874 875
        return result.Component.renderReqToHTML(req, res)
      }

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

    // Toggle whether or not this is an SPR Data request
876 877
    const isDataReq = query._nextDataReq
    delete query._nextDataReq
878

J
JJ Kasper 已提交
879
    // Compute the SPR cache key
880
    const ssgCacheKey = parseUrl(req.url || '').pathname!
J
JJ Kasper 已提交
881 882

    // Complete the response with cached data if its present
883
    const cachedData = await getSprCache(ssgCacheKey)
J
JJ Kasper 已提交
884
    if (cachedData) {
885
      const data = isDataReq
J
JJ Kasper 已提交
886 887 888 889 890 891
        ? JSON.stringify(cachedData.pageData)
        : cachedData.html

      this.__sendPayload(
        res,
        data,
892
        isDataReq ? 'application/json' : 'text/html; charset=utf-8',
J
JJ Kasper 已提交
893
        cachedData.curRevalidate
J
JJ Kasper 已提交
894 895 896 897 898 899
      )

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

J
JJ Kasper 已提交
902 903 904 905
    // 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)
906
    if (isLikeServerless && isDataReq) {
J
JJ Kasper 已提交
907 908 909
      let { pathname } = parseUrl(req.url || '', true)
      pathname = !pathname || pathname === '/' ? '/index' : pathname
      req.url = `/_next/data/${this.buildId}${pathname}.json`
J
JJ Kasper 已提交
910 911 912 913
    }

    const doRender = withCoalescedInvoke(async function(): Promise<{
      html: string | null
914
      pageData: any
J
JJ Kasper 已提交
915 916
      sprRevalidate: number | false
    }> {
917
      let pageData: any
J
JJ Kasper 已提交
918 919 920 921 922 923 924 925 926
      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
927
        pageData = renderResult.renderOpts.pageData
J
JJ Kasper 已提交
928 929 930 931 932 933 934 935 936
        sprRevalidate = renderResult.renderOpts.revalidate
      } else {
        const renderOpts = {
          ...result,
          ...opts,
        }
        renderResult = await renderToHTML(req, res, pathname, query, renderOpts)

        html = renderResult
937
        pageData = renderOpts.pageData
J
JJ Kasper 已提交
938 939 940
        sprRevalidate = renderOpts.revalidate
      }

941
      return { html, pageData, sprRevalidate }
942
    })
J
JJ Kasper 已提交
943

944 945
    return doRender(ssgCacheKey, []).then(
      async ({ isOrigin, value: { html, pageData, sprRevalidate } }) => {
J
JJ Kasper 已提交
946 947 948 949
        // Respond to the request if a payload wasn't sent above (from cache)
        if (!isResSent(res)) {
          this.__sendPayload(
            res,
950 951
            isDataReq ? JSON.stringify(pageData) : html,
            isDataReq ? 'application/json' : 'text/html; charset=utf-8',
J
JJ Kasper 已提交
952
            sprRevalidate
J
JJ Kasper 已提交
953 954 955 956 957 958
          )
        }

        // Update the SPR cache if the head request
        if (isOrigin) {
          await setSprCache(
959 960
            ssgCacheKey,
            { html: html!, pageData },
J
JJ Kasper 已提交
961 962 963 964 965 966 967
            sprRevalidate
          )
        }

        return null
      }
    )
968 969
  }

J
Joe Haddad 已提交
970
  public renderToHTML(
J
Joe Haddad 已提交
971 972 973 974
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
J
Joe Haddad 已提交
975 976 977 978 979 980
    {
      amphtml,
      hasAmp,
    }: {
      amphtml?: boolean
      hasAmp?: boolean
981
    } = {}
J
Joe Haddad 已提交
982
  ): Promise<string | null> {
J
Joe Haddad 已提交
983 984
    return this.findPageComponents(pathname, query)
      .then(
985
        result => {
J
Joe Haddad 已提交
986 987 988 989
          return this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
990
            result.unstable_getStaticProps
991
              ? { _nextDataReq: query._nextDataReq }
992
              : query,
J
Joe Haddad 已提交
993
            result,
T
Tim Neutkens 已提交
994
            { ...this.renderOpts, amphtml, hasAmp }
J
Joe Haddad 已提交
995 996
          )
        },
997
        err => {
J
Joe Haddad 已提交
998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008
          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(
1009 1010
              result => {
                return this.renderToHTMLWithComponents(
J
Joe Haddad 已提交
1011 1012 1013
                  req,
                  res,
                  dynamicRoute.page,
J
JJ Kasper 已提交
1014 1015 1016
                  // only add params for SPR enabled pages
                  {
                    ...(result.unstable_getStaticProps
1017
                      ? { _nextDataReq: query._nextDataReq }
J
JJ Kasper 已提交
1018 1019 1020
                      : query),
                    ...params,
                  },
J
Joe Haddad 已提交
1021
                  result,
J
JJ Kasper 已提交
1022 1023 1024 1025 1026
                  {
                    ...this.renderOpts,
                    amphtml,
                    hasAmp,
                  }
1027
                )
1028
              }
J
Joe Haddad 已提交
1029 1030 1031 1032
            )
          }

          return Promise.reject(err)
1033
        }
J
Joe Haddad 已提交
1034
      )
1035
      .catch(err => {
J
Joe Haddad 已提交
1036 1037 1038 1039 1040 1041 1042 1043 1044
        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 已提交
1045 1046
  }

J
Joe Haddad 已提交
1047 1048 1049 1050 1051
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1052
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1053 1054 1055
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
1056
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
1057
    )
N
Naoyuki Kanezawa 已提交
1058
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
1059
    if (html === null) {
1060 1061
      return
    }
1062
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
1063 1064
  }

J
Joe Haddad 已提交
1065 1066 1067 1068 1069
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
1070
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1071
  ) {
1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088
    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)
    }

1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107
    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 已提交
1108 1109
  }

J
Joe Haddad 已提交
1110 1111 1112
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
1113
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1114
  ): Promise<void> {
1115 1116
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
1117
    if (!pathname) {
1118 1119
      throw new Error('pathname is undefined')
    }
N
Naoyuki Kanezawa 已提交
1120
    res.statusCode = 404
1121
    return this.renderError(null, req, res, pathname, query)
N
Naoyuki Kanezawa 已提交
1122
  }
N
Naoyuki Kanezawa 已提交
1123

J
Joe Haddad 已提交
1124 1125 1126 1127
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
1128
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1129
  ): Promise<void> {
A
Arunoda Susiripala 已提交
1130
    if (!this.isServeableUrl(path)) {
1131
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
1132 1133
    }

1134 1135 1136 1137 1138 1139
    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 已提交
1140
    try {
1141
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
1142
    } catch (err) {
T
Tim Neutkens 已提交
1143
      if (err.code === 'ENOENT' || err.statusCode === 404) {
1144
        this.render404(req, res, parsedUrl)
1145 1146 1147
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
1148 1149 1150 1151 1152 1153
      } else {
        throw err
      }
    }
  }

1154
  private isServeableUrl(path: string): boolean {
A
Arunoda Susiripala 已提交
1155 1156
    const resolved = resolve(path)
    if (
1157
      resolved.indexOf(join(this.distDir) + sep) !== 0 &&
1158 1159
      resolved.indexOf(join(this.dir, 'static') + sep) !== 0 &&
      resolved.indexOf(join(this.dir, 'public') + sep) !== 0
A
Arunoda Susiripala 已提交
1160 1161 1162 1163 1164 1165 1166 1167
    ) {
      // Seems like the user is trying to traverse the filesystem.
      return false
    }

    return true
  }

1168
  protected readBuildId(): string {
1169 1170 1171 1172 1173
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
1174
        throw new Error(
1175
          `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 已提交
1176
        )
1177 1178 1179
      }

      throw err
1180
    }
1181
  }
1182 1183 1184 1185

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