next-server.ts 29.5 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,
20
  DEFAULT_REDIRECT_STATUS,
T
Tim Neutkens 已提交
21
} from '../lib/constants'
J
Joe Haddad 已提交
22 23 24 25
import {
  getRouteMatcher,
  getRouteRegex,
  getSortedRoutes,
26
  isDynamicRoute,
J
Joe Haddad 已提交
27
} from '../lib/router/utils'
28
import * as envConfig from '../lib/runtime-config'
J
Joe Haddad 已提交
29
import { isResSent, NextApiRequest, NextApiResponse } from '../lib/utils'
30
import { apiResolver } from './api-utils'
31
import loadConfig, { isTargetLikeServerless } from './config'
32
import pathMatch from './lib/path-match'
J
Joe Haddad 已提交
33
import { recursiveReadDirSync } from './lib/recursive-readdir-sync'
34
import { loadComponents, LoadComponentsReturnType } from './load-components'
J
Joe Haddad 已提交
35
import { renderToHTML } from './render'
J
Joe Haddad 已提交
36
import { getPagePath } from './require'
37
import Router, { Params, route, Route, RouteMatch } from './router'
J
Joe Haddad 已提交
38 39
import { sendHTML } from './send-html'
import { serveStatic } from './serve-static'
J
Joe Haddad 已提交
40
import { getSprCache, initializeSprCache, setSprCache } from './spr-cache'
41
import { isBlockedPage } from './utils'
42
import { Redirect, Rewrite } from '../../lib/check-custom-routes'
J
JJ Kasper 已提交
43 44

const getCustomRouteMatcher = pathMatch(true)
45 46 47

type NextConfig = any

48 49 50 51 52 53
type Middleware = (
  req: IncomingMessage,
  res: ServerResponse,
  next: (err?: Error) => void
) => void

T
Tim Neutkens 已提交
54
export type ServerConstructor = {
55 56 57
  /**
   * Where the Next project is located - @default '.'
   */
J
Joe Haddad 已提交
58 59
  dir?: string
  staticMarkup?: boolean
60 61 62
  /**
   * Hide error messages containing server information - @default false
   */
J
Joe Haddad 已提交
63
  quiet?: boolean
64 65 66
  /**
   * Object what you would use in next.config.js - @default {}
   */
67
  conf?: NextConfig
J
JJ Kasper 已提交
68
  dev?: boolean
69
}
70

N
nkzawa 已提交
71
export default class Server {
72 73 74 75
  dir: string
  quiet: boolean
  nextConfig: NextConfig
  distDir: string
76
  pagesDir?: string
77
  publicDir: string
78
  hasStaticDir: boolean
79 80
  serverBuildDir: string
  pagesManifest?: { [name: string]: string }
81 82
  buildId: string
  renderOpts: {
T
Tim Neutkens 已提交
83
    poweredByHeader: boolean
T
Tim Neutkens 已提交
84
    ampBindInitData: boolean
J
Joe Haddad 已提交
85 86 87 88
    staticMarkup: boolean
    buildId: string
    generateEtags: boolean
    runtimeConfig?: { [key: string]: any }
89 90
    assetPrefix?: string
    canonicalBase: string
91
    documentMiddlewareEnabled: boolean
J
Joe Haddad 已提交
92
    hasCssMode: boolean
93
    dev?: boolean
94
  }
95
  private compression?: Middleware
J
JJ Kasper 已提交
96
  private onErrorMiddleware?: ({ err }: { err: Error }) => Promise<void>
97
  router: Router
98
  protected dynamicRoutes?: Array<{ page: string; match: RouteMatch }>
J
JJ Kasper 已提交
99 100 101 102
  protected customRoutes?: {
    rewrites: Rewrite[]
    redirects: Redirect[]
  }
103

J
Joe Haddad 已提交
104 105 106 107 108
  public constructor({
    dir = '.',
    staticMarkup = false,
    quiet = false,
    conf = null,
J
JJ Kasper 已提交
109
    dev = false,
J
Joe Haddad 已提交
110
  }: ServerConstructor = {}) {
N
nkzawa 已提交
111
    this.dir = resolve(dir)
N
Naoyuki Kanezawa 已提交
112
    this.quiet = quiet
T
Tim Neutkens 已提交
113
    const phase = this.currentPhase()
114
    this.nextConfig = loadConfig(phase, this.dir, conf)
115
    this.distDir = join(this.dir, this.nextConfig.distDir)
116
    this.publicDir = join(this.dir, CLIENT_PUBLIC_FILES_PATH)
117
    this.hasStaticDir = fs.existsSync(join(this.dir, 'static'))
T
Tim Neutkens 已提交
118

119 120
    // Only serverRuntimeConfig needs the default
    // publicRuntimeConfig gets it's default in client/index.js
J
Joe Haddad 已提交
121 122 123 124 125
    const {
      serverRuntimeConfig = {},
      publicRuntimeConfig,
      assetPrefix,
      generateEtags,
126
      compress,
J
Joe Haddad 已提交
127
    } = this.nextConfig
128

T
Tim Neutkens 已提交
129
    this.buildId = this.readBuildId()
130

131
    this.renderOpts = {
T
Tim Neutkens 已提交
132
      ampBindInitData: this.nextConfig.experimental.ampBindInitData,
T
Tim Neutkens 已提交
133
      poweredByHeader: this.nextConfig.poweredByHeader,
134
      canonicalBase: this.nextConfig.amp.canonicalBase,
135 136
      documentMiddlewareEnabled: this.nextConfig.experimental
        .documentMiddleware,
J
Joe Haddad 已提交
137
      hasCssMode: this.nextConfig.experimental.css,
138
      staticMarkup,
139
      buildId: this.buildId,
140
      generateEtags,
141
    }
N
Naoyuki Kanezawa 已提交
142

143 144
    // Only the `publicRuntimeConfig` key is exposed to the client side
    // It'll be rendered as part of __NEXT_DATA__ on the client side
145
    if (Object.keys(publicRuntimeConfig).length > 0) {
146
      this.renderOpts.runtimeConfig = publicRuntimeConfig
147 148
    }

149
    if (compress && this.nextConfig.target === 'server') {
150 151 152
      this.compression = compression() as Middleware
    }

153
    // Initialize next/config with the environment configuration
154 155 156 157 158 159
    if (this.nextConfig.target === 'server') {
      envConfig.setConfig({
        serverRuntimeConfig,
        publicRuntimeConfig,
      })
    }
160

161 162 163 164 165 166 167 168 169 170
    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 已提交
171
    this.router = new Router(this.generateRoutes())
172
    this.setAssetPrefix(assetPrefix)
J
JJ Kasper 已提交
173

174 175 176
    // call init-server middleware, this is also handled
    // individually in serverless bundles when deployed
    if (!dev && this.nextConfig.experimental.plugins) {
177 178
      const initServer = require(join(this.serverBuildDir, 'init-server.js'))
        .default
179
      this.onErrorMiddleware = require(join(
180
        this.serverBuildDir,
181 182 183 184 185
        'on-error-server.js'
      )).default
      initServer()
    }

J
JJ Kasper 已提交
186 187 188 189 190 191 192 193 194 195 196 197
    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 已提交
198
  }
N
nkzawa 已提交
199

200
  protected currentPhase(): string {
201
    return PHASE_PRODUCTION_SERVER
202 203
  }

204 205 206 207
  private logError(err: Error): void {
    if (this.onErrorMiddleware) {
      this.onErrorMiddleware({ err })
    }
208 209
    if (this.quiet) return
    // tslint:disable-next-line
210
    console.error(err)
211 212
  }

J
Joe Haddad 已提交
213 214 215
  private handleRequest(
    req: IncomingMessage,
    res: ServerResponse,
216
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
217
  ): Promise<void> {
218
    // Parse url if parsedUrl not provided
219
    if (!parsedUrl || typeof parsedUrl !== 'object') {
220 221
      const url: any = req.url
      parsedUrl = parseUrl(url, true)
222
    }
223

224 225 226
    // 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 已提交
227
    }
228

229
    res.statusCode = 200
230
    return this.run(req, res, parsedUrl).catch(err => {
J
Joe Haddad 已提交
231 232 233 234
      this.logError(err)
      res.statusCode = 500
      res.end('Internal Server Error')
    })
235 236
  }

237
  public getRequestHandler() {
238
    return this.handleRequest.bind(this)
N
nkzawa 已提交
239 240
  }

241
  public setAssetPrefix(prefix?: string) {
242
    this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
243 244
  }

245
  // Backwards compatibility
246
  public async prepare(): Promise<void> {}
N
nkzawa 已提交
247

T
Tim Neutkens 已提交
248
  // Backwards compatibility
249
  protected async close(): Promise<void> {}
T
Tim Neutkens 已提交
250

251
  protected setImmutableAssetCacheControl(res: ServerResponse) {
T
Tim Neutkens 已提交
252
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
N
nkzawa 已提交
253 254
  }

J
JJ Kasper 已提交
255 256 257 258
  protected getCustomRoutes() {
    return require(join(this.distDir, ROUTES_MANIFEST))
  }

259
  protected generateRoutes(): Route[] {
J
JJ Kasper 已提交
260 261
    this.customRoutes = this.getCustomRoutes()

262 263 264
    const publicRoutes = fs.existsSync(this.publicDir)
      ? this.generatePublicRoutes()
      : []
J
JJ Kasper 已提交
265

266
    const staticFilesRoute = this.hasStaticDir
267 268 269 270 271 272 273
      ? [
          {
            // 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*'),
274
            name: 'static catchall',
275 276 277
            fn: async (req, res, params, parsedUrl) => {
              const p = join(this.dir, 'static', ...(params.path || []))
              await this.serveStatic(req, res, p, parsedUrl)
278 279 280
              return {
                finished: true,
              }
281 282 283 284
            },
          } as Route,
        ]
      : []
285

286
    const topRoutes: Route[] = [
T
Tim Neutkens 已提交
287
      {
288
        match: route('/_next/static/:path*'),
289 290
        type: 'route',
        name: '_next/static catchall',
291
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
292 293 294
          // 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.
295 296

          // make sure to 404 for /_next/static itself
297 298 299 300 301 302
          if (!params.path) {
            await this.render404(req, res, parsedUrl)
            return {
              finished: true,
            }
          }
303

J
Joe Haddad 已提交
304 305 306 307 308
          if (
            params.path[0] === CLIENT_STATIC_FILES_RUNTIME ||
            params.path[0] === 'chunks' ||
            params.path[0] === this.buildId
          ) {
T
Tim Neutkens 已提交
309
            this.setImmutableAssetCacheControl(res)
310
          }
J
Joe Haddad 已提交
311 312 313
          const p = join(
            this.distDir,
            CLIENT_STATIC_FILES_PATH,
314
            ...(params.path || [])
J
Joe Haddad 已提交
315
          )
316
          await this.serveStatic(req, res, p, parsedUrl)
317 318 319
          return {
            finished: true,
          }
320
        },
321
      },
J
JJ Kasper 已提交
322 323
      {
        match: route('/_next/data/:path*'),
324 325
        type: 'route',
        name: '_next/data catchall',
J
JJ Kasper 已提交
326
        fn: async (req, res, params, _parsedUrl) => {
J
JJ Kasper 已提交
327 328 329
          // 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) {
330 331 332 333
            await this.render404(req, res, _parsedUrl)
            return {
              finished: true,
            }
J
JJ Kasper 已提交
334 335 336 337 338 339
          }
          // 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')) {
340 341 342 343
            await this.render404(req, res, _parsedUrl)
            return {
              finished: true,
            }
J
JJ Kasper 已提交
344 345 346 347 348 349 350
          }

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

J
JJ Kasper 已提交
351 352 353 354 355 356 357 358 359
          req.url = pathname
          const parsedUrl = parseUrl(pathname, true)
          await this.render(
            req,
            res,
            pathname,
            { _nextSprData: '1' },
            parsedUrl
          )
360 361 362
          return {
            finished: true,
          }
J
JJ Kasper 已提交
363 364
        },
      },
T
Tim Neutkens 已提交
365
      {
366
        match: route('/_next/:path*'),
367 368
        type: 'route',
        name: '_next catchall',
T
Tim Neutkens 已提交
369
        // This path is needed because `render()` does a check for `/_next` and the calls the routing again
370
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
371
          await this.render404(req, res, parsedUrl)
372 373 374
          return {
            finished: true,
          }
L
Lukáš Huvar 已提交
375 376
        },
      },
377 378
      ...publicRoutes,
      ...staticFilesRoute,
T
Tim Neutkens 已提交
379
    ]
380
    const routes: Route[] = [...topRoutes]
381

J
JJ Kasper 已提交
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399
    if (this.customRoutes) {
      const { redirects, rewrites } = this.customRoutes

      const getCustomRoute = (
        r: { source: string; destination: string; statusCode?: number },
        type: 'redirect' | 'rewrite'
      ) => ({
        ...r,
        type,
        matcher: getCustomRouteMatcher(r.source),
      })

      const customRoutes = [
        ...redirects.map(r => getCustomRoute(r, 'redirect')),
        ...rewrites.map(r => getCustomRoute(r, 'rewrite')),
      ]

      routes.push(
400
        ...customRoutes.map(route => {
J
JJ Kasper 已提交
401
          return {
402 403 404 405 406
            match: route.matcher,
            type: route.type,
            statusCode: route.statusCode,
            name: `${route.type} ${route.source} route`,
            fn: async (_req, res, params, _parsedUrl) => {
J
Joe Haddad 已提交
407
              let destinationCompiler = compilePathToRegex(route.destination)
408 409 410 411 412 413
              let newUrl

              try {
                newUrl = destinationCompiler(params)
              } catch (err) {
                if (
J
Joe Haddad 已提交
414 415 416
                  err.message.match(
                    /Expected .*? to not repeat, but got an array/
                  )
417 418 419 420 421 422 423
                ) {
                  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
              }
424 425 426 427 428 429 430

              if (route.type === 'redirect') {
                res.setHeader('Location', newUrl)
                res.statusCode = route.statusCode || DEFAULT_REDIRECT_STATUS
                res.end()
                return {
                  finished: true,
J
JJ Kasper 已提交
431 432 433
                }
              }

434 435 436
              return {
                finished: false,
                pathname: newUrl,
J
JJ Kasper 已提交
437 438 439 440 441
              }
            },
          } as Route
        })
      )
442 443 444
      // make sure previous routes can still be rewritten to by
      // custom routes e.g. /docs/_next/static -> /_next/static
      routes.push(...topRoutes)
J
JJ Kasper 已提交
445 446
    }

447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462
    routes.push({
      match: route('/api/:path*'),
      type: 'route',
      name: 'API Route',
      fn: async (req, res, params, parsedUrl) => {
        const { pathname } = parsedUrl
        await this.handleApiRequest(
          req as NextApiRequest,
          res as NextApiResponse,
          pathname!
        )
        return {
          finished: true,
        }
      },
    })
463

464
    if (this.nextConfig.useFileSystemPublicRoutes) {
J
Joe Haddad 已提交
465
      this.dynamicRoutes = this.getDynamicRoutes()
J
Joe Haddad 已提交
466

467 468 469
      // 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.
470
      // See more: https://github.com/zeit/next.js/issues/2617
T
Tim Neutkens 已提交
471
      routes.push({
472
        match: route('/:path*'),
473 474 475
        type: 'route',
        name: 'Catchall render',
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
476
          const { pathname, query } = parsedUrl
477
          if (!pathname) {
478 479
            throw new Error('pathname is undefined')
          }
480

481 482 483 484 485 486 487
          // Used in development to check public directory paths
          if (await this._beforeCatchAllRender(req, res, params, parsedUrl)) {
            return {
              finished: true,
            }
          }

488
          await this.render(req, res, pathname, query, parsedUrl)
489 490 491
          return {
            finished: true,
          }
492
        },
T
Tim Neutkens 已提交
493
      })
494
    }
N
nkzawa 已提交
495

T
Tim Neutkens 已提交
496 497 498
    return routes
  }

499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516
  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
  }

517 518 519 520 521 522 523 524 525
  protected async _beforeCatchAllRender(
    _req: IncomingMessage,
    _res: ServerResponse,
    _params: Params,
    _parsedUrl: UrlWithParsedQuery
  ) {
    return false
  }

526 527 528
  // Used to build API page in development
  protected async ensureApiPage(pathname: string) {}

L
Lukáš Huvar 已提交
529 530 531 532 533 534
  /**
   * Resolves `API` request, in development builds on demand
   * @param req http request
   * @param res http response
   * @param pathname path of request
   */
J
Joe Haddad 已提交
535
  private async handleApiRequest(
536 537
    req: IncomingMessage,
    res: ServerResponse,
538
    pathname: string
J
Joe Haddad 已提交
539
  ) {
540
    let page = pathname
L
Lukáš Huvar 已提交
541
    let params: Params | boolean = false
542
    let pageFound = await this.hasPage(page)
J
JJ Kasper 已提交
543

544
    if (!pageFound && this.dynamicRoutes) {
L
Lukáš Huvar 已提交
545 546 547
      for (const dynamicRoute of this.dynamicRoutes) {
        params = dynamicRoute.match(pathname)
        if (params) {
548 549
          page = dynamicRoute.page
          pageFound = true
L
Lukáš Huvar 已提交
550 551 552 553 554
          break
        }
      }
    }

555
    if (!pageFound) {
J
JJ Kasper 已提交
556 557
      return this.render404(req, res)
    }
558 559 560 561 562 563
    // 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 已提交
564

565
    if (!this.renderOpts.dev && this._isLikeServerless) {
566 567
      if (typeof pageModule.default === 'function') {
        return pageModule.default(req, res)
J
JJ Kasper 已提交
568 569 570
      }
    }

571
    await apiResolver(req, res, params, pageModule, this.onErrorMiddleware)
L
Lukáš Huvar 已提交
572 573
  }

574
  protected generatePublicRoutes(): Route[] {
575 576 577
    const routes: Route[] = []
    const publicFiles = recursiveReadDirSync(this.publicDir)

578
    publicFiles.forEach(path => {
579 580
      const unixPath = path.replace(/\\/g, '/')
      // Only include public files that will not replace a page path
581 582
      // this should not occur now that we check this during build
      if (!this.pagesManifest![unixPath]) {
583 584
        routes.push({
          match: route(unixPath),
585 586
          type: 'route',
          name: 'public catchall',
587 588 589
          fn: async (req, res, _params, parsedUrl) => {
            const p = join(this.publicDir, unixPath)
            await this.serveStatic(req, res, p, parsedUrl)
590 591 592
            return {
              finished: true,
            }
593 594 595 596 597 598 599 600
          },
        })
      }
    })

    return routes
  }

601
  protected getDynamicRoutes() {
602 603 604
    const dynamicRoutedPages = Object.keys(this.pagesManifest!).filter(
      isDynamicRoute
    )
605 606 607 608
    return getSortedRoutes(dynamicRoutedPages).map(page => ({
      page,
      match: getRouteMatcher(getRouteRegex(page)),
    }))
J
Joe Haddad 已提交
609 610
  }

611 612 613 614 615 616
  private handleCompression(req: IncomingMessage, res: ServerResponse) {
    if (this.compression) {
      this.compression(req, res, () => {})
    }
  }

617
  protected async run(
J
Joe Haddad 已提交
618 619
    req: IncomingMessage,
    res: ServerResponse,
620
    parsedUrl: UrlWithParsedQuery
J
Joe Haddad 已提交
621
  ) {
622 623
    this.handleCompression(req, res)

624
    try {
625 626
      const matched = await this.router.execute(req, res, parsedUrl)
      if (matched) {
627 628 629 630 631 632 633 634
        return
      }
    } catch (err) {
      if (err.code === 'DECODE_FAILED') {
        res.statusCode = 400
        return this.renderError(null, req, res, '/_error', {})
      }
      throw err
635 636
    }

637
    await this.render404(req, res, parsedUrl)
N
nkzawa 已提交
638 639
  }

640
  protected async sendHTML(
J
Joe Haddad 已提交
641 642
    req: IncomingMessage,
    res: ServerResponse,
643
    html: string
J
Joe Haddad 已提交
644
  ) {
T
Tim Neutkens 已提交
645 646
    const { generateEtags, poweredByHeader } = this.renderOpts
    return sendHTML(req, res, html, { generateEtags, poweredByHeader })
647 648
  }

J
Joe Haddad 已提交
649 650 651 652 653
  public async render(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
654
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
655
  ): Promise<void> {
656
    const url: any = req.url
657 658 659 660 661

    if (
      url.match(/^\/_next\//) ||
      (this.hasStaticDir && url.match(/^\/static\//))
    ) {
662 663 664
      return this.handleRequest(req, res, parsedUrl)
    }

665
    if (isBlockedPage(pathname)) {
666
      return this.render404(req, res, parsedUrl)
667 668
    }

669
    const html = await this.renderToHTML(req, res, pathname, query, {
J
Joe Haddad 已提交
670 671 672 673 674
      dataOnly:
        (this.renderOpts.ampBindInitData && Boolean(query.dataOnly)) ||
        (req.headers &&
          (req.headers.accept || '').indexOf('application/amp.bind+json') !==
            -1),
675
    })
676 677
    // Request was ended by the user
    if (html === null) {
678 679 680
      return
    }

681
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
682
  }
N
nkzawa 已提交
683

J
Joe Haddad 已提交
684
  private async findPageComponents(
J
Joe Haddad 已提交
685
    pathname: string,
686
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
687
  ) {
688
    const serverless = !this.renderOpts.dev && this._isLikeServerless
J
JJ Kasper 已提交
689 690 691
    // try serving a static AMP version first
    if (query.amp) {
      try {
J
Joe Haddad 已提交
692 693 694 695
        return await loadComponents(
          this.distDir,
          this.buildId,
          (pathname === '/' ? '/index' : pathname) + '.amp',
696
          serverless
J
Joe Haddad 已提交
697
        )
J
JJ Kasper 已提交
698 699 700 701
      } catch (err) {
        if (err.code !== 'ENOENT') throw err
      }
    }
J
Joe Haddad 已提交
702 703 704 705
    return await loadComponents(
      this.distDir,
      this.buildId,
      pathname,
706
      serverless
J
Joe Haddad 已提交
707 708 709
    )
  }

J
JJ Kasper 已提交
710 711 712 713 714 715
  private __sendPayload(
    res: ServerResponse,
    payload: any,
    type: string,
    revalidate?: number | false
  ) {
J
JJ Kasper 已提交
716
    // TODO: ETag? Cache-Control headers? Next-specific headers?
J
JJ Kasper 已提交
717
    res.setHeader('Content-Type', type)
J
JJ Kasper 已提交
718
    res.setHeader('Content-Length', Buffer.byteLength(payload))
J
Joe Haddad 已提交
719 720 721 722 723 724 725 726 727 728 729 730
    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 已提交
731
    }
J
JJ Kasper 已提交
732 733 734
    res.end(payload)
  }

J
Joe Haddad 已提交
735 736 737 738 739 740
  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
    result: LoadComponentsReturnType,
741
    opts: any
J
JJ Kasper 已提交
742
  ): Promise<string | null> {
J
JJ Kasper 已提交
743
    // handle static page
J
Joe Haddad 已提交
744 745 746 747
    if (typeof result.Component === 'string') {
      return result.Component
    }

J
JJ Kasper 已提交
748 749
    // check request state
    const isLikeServerless =
J
Joe Haddad 已提交
750
      typeof result.Component === 'object' &&
J
JJ Kasper 已提交
751
      typeof result.Component.renderReqToHTML === 'function'
J
JJ Kasper 已提交
752 753 754 755 756 757
    const isSpr = !!result.unstable_getStaticProps

    // non-spr requests should render like normal
    if (!isSpr) {
      // handle serverless
      if (isLikeServerless) {
758 759 760 761 762 763 764 765
        const curUrl = parseUrl(req.url!, true)
        req.url = formatUrl({
          ...curUrl,
          query: {
            ...curUrl.query,
            ...query,
          },
        })
J
JJ Kasper 已提交
766 767 768 769 770 771 772 773 774 775 776
        return result.Component.renderReqToHTML(req, res)
      }

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

    // Toggle whether or not this is an SPR Data request
    const isSprData = isSpr && query._nextSprData
777 778
    delete query._nextSprData

J
JJ Kasper 已提交
779 780 781 782 783 784 785 786 787 788 789 790 791
    // Compute the SPR cache key
    const sprCacheKey = parseUrl(req.url || '').pathname!

    // Complete the response with cached data if its present
    const cachedData = await getSprCache(sprCacheKey)
    if (cachedData) {
      const data = isSprData
        ? JSON.stringify(cachedData.pageData)
        : cachedData.html

      this.__sendPayload(
        res,
        data,
J
JJ Kasper 已提交
792 793
        isSprData ? 'application/json' : 'text/html; charset=utf-8',
        cachedData.curRevalidate
J
JJ Kasper 已提交
794 795 796 797 798 799
      )

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

J
JJ Kasper 已提交
802 803 804 805 806
    // 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)
    if (isLikeServerless && isSprData) {
J
JJ Kasper 已提交
807 808 809
      let { pathname } = parseUrl(req.url || '', true)
      pathname = !pathname || pathname === '/' ? '/index' : pathname
      req.url = `/_next/data/${this.buildId}${pathname}.json`
J
JJ Kasper 已提交
810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841
    }

    const doRender = withCoalescedInvoke(async function(): Promise<{
      html: string | null
      sprData: any
      sprRevalidate: number | false
    }> {
      let sprData: any
      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
        sprData = renderResult.renderOpts.sprData
        sprRevalidate = renderResult.renderOpts.revalidate
      } else {
        const renderOpts = {
          ...result,
          ...opts,
        }
        renderResult = await renderToHTML(req, res, pathname, query, renderOpts)

        html = renderResult
        sprData = renderOpts.sprData
        sprRevalidate = renderOpts.revalidate
      }

      return { html, sprData, sprRevalidate }
842
    })
J
JJ Kasper 已提交
843 844 845 846 847 848 849 850

    return doRender(sprCacheKey, []).then(
      async ({ isOrigin, value: { html, sprData, sprRevalidate } }) => {
        // Respond to the request if a payload wasn't sent above (from cache)
        if (!isResSent(res)) {
          this.__sendPayload(
            res,
            isSprData ? JSON.stringify(sprData) : html,
J
JJ Kasper 已提交
851 852
            isSprData ? 'application/json' : 'text/html; charset=utf-8',
            sprRevalidate
J
JJ Kasper 已提交
853 854 855 856 857 858 859 860 861 862 863 864 865 866 867
          )
        }

        // Update the SPR cache if the head request
        if (isOrigin) {
          await setSprCache(
            sprCacheKey,
            { html: html!, pageData: sprData },
            sprRevalidate
          )
        }

        return null
      }
    )
868 869
  }

J
Joe Haddad 已提交
870
  public renderToHTML(
J
Joe Haddad 已提交
871 872 873 874
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
J
Joe Haddad 已提交
875 876 877 878 879 880 881
    {
      amphtml,
      dataOnly,
      hasAmp,
    }: {
      amphtml?: boolean
      hasAmp?: boolean
882 883
      dataOnly?: boolean
    } = {}
J
Joe Haddad 已提交
884
  ): Promise<string | null> {
J
Joe Haddad 已提交
885 886
    return this.findPageComponents(pathname, query)
      .then(
887
        result => {
J
Joe Haddad 已提交
888 889 890 891
          return this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
892 893 894
            result.unstable_getStaticProps
              ? { _nextSprData: query._nextSprData }
              : query,
J
Joe Haddad 已提交
895
            result,
896
            { ...this.renderOpts, amphtml, hasAmp, dataOnly }
J
Joe Haddad 已提交
897 898
          )
        },
899
        err => {
J
Joe Haddad 已提交
900 901 902 903 904 905 906 907 908 909 910
          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(
911 912
              result => {
                return this.renderToHTMLWithComponents(
J
Joe Haddad 已提交
913 914 915
                  req,
                  res,
                  dynamicRoute.page,
J
JJ Kasper 已提交
916 917 918 919 920 921 922
                  // only add params for SPR enabled pages
                  {
                    ...(result.unstable_getStaticProps
                      ? { _nextSprData: query._nextSprData }
                      : query),
                    ...params,
                  },
J
Joe Haddad 已提交
923
                  result,
J
JJ Kasper 已提交
924 925 926 927 928 929
                  {
                    ...this.renderOpts,
                    amphtml,
                    hasAmp,
                    dataOnly,
                  }
930
                )
931
              }
J
Joe Haddad 已提交
932 933 934 935
            )
          }

          return Promise.reject(err)
936
        }
J
Joe Haddad 已提交
937
      )
938
      .catch(err => {
J
Joe Haddad 已提交
939 940 941 942 943 944 945 946 947
        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 已提交
948 949
  }

J
Joe Haddad 已提交
950 951 952 953 954
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
955
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
956 957 958
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
959
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
960
    )
N
Naoyuki Kanezawa 已提交
961
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
962
    if (html === null) {
963 964
      return
    }
965
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
966 967
  }

J
Joe Haddad 已提交
968 969 970 971 972
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
973
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
974
  ) {
J
Joe Haddad 已提交
975
    const result = await this.findPageComponents('/_error', query)
976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994
    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 已提交
995 996
  }

J
Joe Haddad 已提交
997 998 999
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
1000
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1001
  ): Promise<void> {
1002 1003
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
1004
    if (!pathname) {
1005 1006
      throw new Error('pathname is undefined')
    }
N
Naoyuki Kanezawa 已提交
1007
    res.statusCode = 404
1008
    return this.renderError(null, req, res, pathname, query)
N
Naoyuki Kanezawa 已提交
1009
  }
N
Naoyuki Kanezawa 已提交
1010

J
Joe Haddad 已提交
1011 1012 1013 1014
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
1015
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1016
  ): Promise<void> {
A
Arunoda Susiripala 已提交
1017
    if (!this.isServeableUrl(path)) {
1018
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
1019 1020
    }

1021 1022 1023 1024 1025 1026
    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 已提交
1027
    try {
1028
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
1029
    } catch (err) {
T
Tim Neutkens 已提交
1030
      if (err.code === 'ENOENT' || err.statusCode === 404) {
1031
        this.render404(req, res, parsedUrl)
1032 1033 1034
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
1035 1036 1037 1038 1039 1040
      } else {
        throw err
      }
    }
  }

1041
  private isServeableUrl(path: string): boolean {
A
Arunoda Susiripala 已提交
1042 1043
    const resolved = resolve(path)
    if (
1044
      resolved.indexOf(join(this.distDir) + sep) !== 0 &&
1045 1046
      resolved.indexOf(join(this.dir, 'static') + sep) !== 0 &&
      resolved.indexOf(join(this.dir, 'public') + sep) !== 0
A
Arunoda Susiripala 已提交
1047 1048 1049 1050 1051 1052 1053 1054
    ) {
      // Seems like the user is trying to traverse the filesystem.
      return false
    }

    return true
  }

1055
  protected readBuildId(): string {
1056 1057 1058 1059 1060
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
1061
        throw new Error(
1062
          `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 已提交
1063
        )
1064 1065 1066
      }

      throw err
1067
    }
1068
  }
1069 1070 1071 1072

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