next-server.ts 33.6 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
            { ..._parsedUrl.query, _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
          const handled = await this.handleApiRequest(
            req as NextApiRequest,
            res as NextApiResponse,
564 565
            pathname!,
            query
566 567 568 569 570 571 572
          )
          if (handled) {
            return { finished: true }
          }
        }

        await this.render(req, res, pathname, query, parsedUrl)
573 574 575 576
        return {
          finished: true,
        }
      },
577
    }
578

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

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

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

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

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

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

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

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

655
    if (!pageFound) {
656
      return false
J
JJ Kasper 已提交
657
    }
658 659 660 661 662 663
    // 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)
664
    query = { ...query, ...params }
J
JJ Kasper 已提交
665

666
    if (!this.renderOpts.dev && this._isLikeServerless) {
667
      if (typeof pageModule.default === 'function') {
668
        this.prepareServerlessUrl(req, query)
669 670
        await pageModule.default(req, res)
        return true
J
JJ Kasper 已提交
671 672 673
      }
    }

674
    await apiResolver(req, res, query, pageModule, this.onErrorMiddleware)
675
    return true
L
Lukáš Huvar 已提交
676 677
  }

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

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

720 721 722 723 724 725
  private handleCompression(req: IncomingMessage, res: ServerResponse) {
    if (this.compression) {
      this.compression(req, res, () => {})
    }
  }

726
  protected async run(
J
Joe Haddad 已提交
727 728
    req: IncomingMessage,
    res: ServerResponse,
729
    parsedUrl: UrlWithParsedQuery
J
Joe Haddad 已提交
730
  ) {
731 732
    this.handleCompression(req, res)

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

746
    await this.render404(req, res, parsedUrl)
N
nkzawa 已提交
747 748
  }

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

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

    if (
      url.match(/^\/_next\//) ||
      (this.hasStaticDir && url.match(/^\/static\//))
    ) {
771 772 773
      return this.handleRequest(req, res, parsedUrl)
    }

774
    if (isBlockedPage(pathname)) {
775
      return this.render404(req, res, parsedUrl)
776 777
    }

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

784
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
785
  }
N
nkzawa 已提交
786

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

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

840 841 842 843 844 845 846 847 848 849 850 851
  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 已提交
852 853 854 855 856 857
  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
    result: LoadComponentsReturnType,
858
    opts: any
J
JJ Kasper 已提交
859
  ): Promise<string | null> {
J
JJ Kasper 已提交
860
    // handle static page
J
Joe Haddad 已提交
861 862 863 864
    if (typeof result.Component === 'string') {
      return result.Component
    }

J
JJ Kasper 已提交
865 866
    // check request state
    const isLikeServerless =
J
Joe Haddad 已提交
867
      typeof result.Component === 'object' &&
868
      typeof (result.Component as any).renderReqToHTML === 'function'
869
    const isSSG = !!result.unstable_getStaticProps
870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885
    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 已提交
886 887

    // non-spr requests should render like normal
888
    if (!isSSG) {
J
JJ Kasper 已提交
889 890
      // handle serverless
      if (isLikeServerless) {
891 892 893 894 895 896 897 898 899 900 901 902 903 904 905
        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
        }
906
        this.prepareServerlessUrl(req, query)
907
        return (result.Component as any).renderReqToHTML(req, res)
J
JJ Kasper 已提交
908 909
      }

910 911 912 913 914 915 916 917 918 919
      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 已提交
920 921 922 923 924 925 926
      return renderToHTML(req, res, pathname, query, {
        ...result,
        ...opts,
      })
    }

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

    // Complete the response with cached data if its present
930
    const cachedData = await getSprCache(ssgCacheKey)
J
JJ Kasper 已提交
931
    if (cachedData) {
932
      const data = isDataReq
J
JJ Kasper 已提交
933 934 935 936 937 938
        ? JSON.stringify(cachedData.pageData)
        : cachedData.html

      this.__sendPayload(
        res,
        data,
939
        isDataReq ? 'application/json' : 'text/html; charset=utf-8',
J
JJ Kasper 已提交
940
        cachedData.curRevalidate
J
JJ Kasper 已提交
941 942 943 944 945 946
      )

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

J
JJ Kasper 已提交
949 950 951 952
    // If we're here, that means data is missing or it's stale.

    const doRender = withCoalescedInvoke(async function(): Promise<{
      html: string | null
953
      pageData: any
J
JJ Kasper 已提交
954 955
      sprRevalidate: number | false
    }> {
956
      let pageData: any
J
JJ Kasper 已提交
957 958 959 960 961 962
      let html: string | null
      let sprRevalidate: number | false

      let renderResult
      // handle serverless
      if (isLikeServerless) {
963 964 965 966 967
        renderResult = await (result.Component as any).renderReqToHTML(
          req,
          res,
          true
        )
J
JJ Kasper 已提交
968 969

        html = renderResult.html
970
        pageData = renderResult.renderOpts.pageData
J
JJ Kasper 已提交
971 972 973 974 975 976 977 978 979
        sprRevalidate = renderResult.renderOpts.revalidate
      } else {
        const renderOpts = {
          ...result,
          ...opts,
        }
        renderResult = await renderToHTML(req, res, pathname, query, renderOpts)

        html = renderResult
980
        pageData = renderOpts.pageData
J
JJ Kasper 已提交
981 982 983
        sprRevalidate = renderOpts.revalidate
      }

984
      return { html, pageData, sprRevalidate }
985
    })
J
JJ Kasper 已提交
986

987 988
    return doRender(ssgCacheKey, []).then(
      async ({ isOrigin, value: { html, pageData, sprRevalidate } }) => {
J
JJ Kasper 已提交
989 990 991 992
        // Respond to the request if a payload wasn't sent above (from cache)
        if (!isResSent(res)) {
          this.__sendPayload(
            res,
993 994
            isDataReq ? JSON.stringify(pageData) : html,
            isDataReq ? 'application/json' : 'text/html; charset=utf-8',
J
JJ Kasper 已提交
995
            sprRevalidate
J
JJ Kasper 已提交
996 997 998 999 1000 1001
          )
        }

        // Update the SPR cache if the head request
        if (isOrigin) {
          await setSprCache(
1002 1003
            ssgCacheKey,
            { html: html!, pageData },
J
JJ Kasper 已提交
1004 1005 1006 1007 1008 1009 1010
            sprRevalidate
          )
        }

        return null
      }
    )
1011 1012
  }

J
Joe Haddad 已提交
1013
  public renderToHTML(
J
Joe Haddad 已提交
1014 1015 1016 1017
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
J
Joe Haddad 已提交
1018 1019 1020 1021 1022 1023
    {
      amphtml,
      hasAmp,
    }: {
      amphtml?: boolean
      hasAmp?: boolean
1024
    } = {}
J
Joe Haddad 已提交
1025
  ): Promise<string | null> {
J
Joe Haddad 已提交
1026 1027
    return this.findPageComponents(pathname, query)
      .then(
1028
        result => {
J
Joe Haddad 已提交
1029 1030 1031 1032
          return this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
1033
            result.unstable_getStaticProps
1034
              ? { _nextDataReq: query._nextDataReq }
1035
              : query,
J
Joe Haddad 已提交
1036
            result,
T
Tim Neutkens 已提交
1037
            { ...this.renderOpts, amphtml, hasAmp }
J
Joe Haddad 已提交
1038 1039
          )
        },
1040
        err => {
J
Joe Haddad 已提交
1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051
          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(
1052 1053
              result => {
                return this.renderToHTMLWithComponents(
J
Joe Haddad 已提交
1054 1055 1056
                  req,
                  res,
                  dynamicRoute.page,
J
JJ Kasper 已提交
1057 1058 1059
                  // only add params for SPR enabled pages
                  {
                    ...(result.unstable_getStaticProps
1060
                      ? { _nextDataReq: query._nextDataReq }
J
JJ Kasper 已提交
1061 1062 1063
                      : query),
                    ...params,
                  },
J
Joe Haddad 已提交
1064
                  result,
J
JJ Kasper 已提交
1065 1066
                  {
                    ...this.renderOpts,
1067
                    params,
J
JJ Kasper 已提交
1068 1069 1070
                    amphtml,
                    hasAmp,
                  }
1071
                )
1072
              }
J
Joe Haddad 已提交
1073 1074 1075 1076
            )
          }

          return Promise.reject(err)
1077
        }
J
Joe Haddad 已提交
1078
      )
1079
      .catch(err => {
J
Joe Haddad 已提交
1080 1081 1082 1083 1084 1085 1086 1087 1088
        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 已提交
1089 1090
  }

J
Joe Haddad 已提交
1091 1092 1093 1094 1095
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
1096
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1097 1098 1099
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
1100
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
1101
    )
N
Naoyuki Kanezawa 已提交
1102
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
1103
    if (html === null) {
1104 1105
      return
    }
1106
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
1107 1108
  }

J
Joe Haddad 已提交
1109 1110 1111 1112 1113
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
1114
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
1115
  ) {
1116 1117 1118
    let result: null | LoadComponentsReturnType = null

    // use static 404 page if available and is 404 response
1119
    if (this.nextConfig.experimental.static404 && res.statusCode === 404) {
1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132
      try {
        result = await this.findPageComponents('/_errors/404')
      } catch (err) {
        if (err.code !== 'ENOENT') {
          throw err
        }
      }
    }

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

1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151
    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 已提交
1152 1153
  }

J
Joe Haddad 已提交
1154 1155 1156
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
1157
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1158
  ): Promise<void> {
1159 1160
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
1161
    if (!pathname) {
1162 1163
      throw new Error('pathname is undefined')
    }
N
Naoyuki Kanezawa 已提交
1164
    res.statusCode = 404
1165
    return this.renderError(null, req, res, pathname, query)
N
Naoyuki Kanezawa 已提交
1166
  }
N
Naoyuki Kanezawa 已提交
1167

J
Joe Haddad 已提交
1168 1169 1170 1171
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
1172
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1173
  ): Promise<void> {
A
Arunoda Susiripala 已提交
1174
    if (!this.isServeableUrl(path)) {
1175
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
1176 1177
    }

1178 1179 1180 1181 1182 1183
    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 已提交
1184
    try {
1185
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
1186
    } catch (err) {
T
Tim Neutkens 已提交
1187
      if (err.code === 'ENOENT' || err.statusCode === 404) {
1188
        this.render404(req, res, parsedUrl)
1189 1190 1191
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
1192 1193 1194 1195 1196 1197
      } else {
        throw err
      }
    }
  }

1198
  private isServeableUrl(path: string): boolean {
A
Arunoda Susiripala 已提交
1199 1200
    const resolved = resolve(path)
    if (
1201
      resolved.indexOf(join(this.distDir) + sep) !== 0 &&
1202 1203
      resolved.indexOf(join(this.dir, 'static') + sep) !== 0 &&
      resolved.indexOf(join(this.dir, 'public') + sep) !== 0
A
Arunoda Susiripala 已提交
1204 1205 1206 1207 1208 1209 1210 1211
    ) {
      // Seems like the user is trying to traverse the filesystem.
      return false
    }

    return true
  }

1212
  protected readBuildId(): string {
1213 1214 1215 1216 1217
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
1218
        throw new Error(
1219
          `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 已提交
1220
        )
1221 1222 1223
      }

      throw err
1224
    }
1225
  }
1226 1227 1228 1229

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