next-server.ts 30.0 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 38 39 40 41 42 43
import Router, {
  Params,
  route,
  Route,
  DynamicRoutes,
  PageChecker,
} from './router'
J
Joe Haddad 已提交
44 45
import { sendHTML } from './send-html'
import { serveStatic } from './serve-static'
J
Joe Haddad 已提交
46
import { getSprCache, initializeSprCache, setSprCache } from './spr-cache'
47
import { isBlockedPage } from './utils'
48
import { Redirect, Rewrite } from '../../lib/check-custom-routes'
J
JJ Kasper 已提交
49 50

const getCustomRouteMatcher = pathMatch(true)
51 52 53

type NextConfig = any

54 55 56 57 58 59
type Middleware = (
  req: IncomingMessage,
  res: ServerResponse,
  next: (err?: Error) => void
) => void

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

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

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

125 126
    // Only serverRuntimeConfig needs the default
    // publicRuntimeConfig gets it's default in client/index.js
J
Joe Haddad 已提交
127 128 129 130 131
    const {
      serverRuntimeConfig = {},
      publicRuntimeConfig,
      assetPrefix,
      generateEtags,
132
      compress,
J
Joe Haddad 已提交
133
    } = this.nextConfig
134

T
Tim Neutkens 已提交
135
    this.buildId = this.readBuildId()
136

137
    this.renderOpts = {
T
Tim Neutkens 已提交
138
      ampBindInitData: this.nextConfig.experimental.ampBindInitData,
T
Tim Neutkens 已提交
139
      poweredByHeader: this.nextConfig.poweredByHeader,
140
      canonicalBase: this.nextConfig.amp.canonicalBase,
141 142
      documentMiddlewareEnabled: this.nextConfig.experimental
        .documentMiddleware,
J
Joe Haddad 已提交
143
      hasCssMode: this.nextConfig.experimental.css,
144
      staticMarkup,
145
      buildId: this.buildId,
146
      generateEtags,
147
    }
N
Naoyuki Kanezawa 已提交
148

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

155
    if (compress && this.nextConfig.target === 'server') {
156 157 158
      this.compression = compression() as Middleware
    }

159
    // Initialize next/config with the environment configuration
160 161 162 163 164 165
    if (this.nextConfig.target === 'server') {
      envConfig.setConfig({
        serverRuntimeConfig,
        publicRuntimeConfig,
      })
    }
166

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

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

J
JJ Kasper 已提交
192 193 194 195 196 197 198 199 200 201 202 203
    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 已提交
204
  }
N
nkzawa 已提交
205

206
  protected currentPhase(): string {
207
    return PHASE_PRODUCTION_SERVER
208 209
  }

210 211 212 213
  private logError(err: Error): void {
    if (this.onErrorMiddleware) {
      this.onErrorMiddleware({ err })
    }
214 215
    if (this.quiet) return
    // tslint:disable-next-line
216
    console.error(err)
217 218
  }

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

230 231 232
    // 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 已提交
233
    }
234

235
    res.statusCode = 200
236
    return this.run(req, res, parsedUrl).catch(err => {
J
Joe Haddad 已提交
237 238 239 240
      this.logError(err)
      res.statusCode = 500
      res.end('Internal Server Error')
    })
241 242
  }

243
  public getRequestHandler() {
244
    return this.handleRequest.bind(this)
N
nkzawa 已提交
245 246
  }

247
  public setAssetPrefix(prefix?: string) {
248
    this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
249 250
  }

251
  // Backwards compatibility
252
  public async prepare(): Promise<void> {}
N
nkzawa 已提交
253

T
Tim Neutkens 已提交
254
  // Backwards compatibility
255
  protected async close(): Promise<void> {}
T
Tim Neutkens 已提交
256

257
  protected setImmutableAssetCacheControl(res: ServerResponse) {
T
Tim Neutkens 已提交
258
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
N
nkzawa 已提交
259 260
  }

J
JJ Kasper 已提交
261 262 263 264
  protected getCustomRoutes() {
    return require(join(this.distDir, ROUTES_MANIFEST))
  }

265 266 267 268 269 270 271
  protected generateRoutes(): {
    routes: Route[]
    fsRoutes: Route[]
    catchAllRoute: Route
    pageChecker: PageChecker
    dynamicRoutes: DynamicRoutes | undefined
  } {
J
JJ Kasper 已提交
272 273
    this.customRoutes = this.getCustomRoutes()

274 275 276
    const publicRoutes = fs.existsSync(this.publicDir)
      ? this.generatePublicRoutes()
      : []
J
JJ Kasper 已提交
277

278
    const staticFilesRoute = this.hasStaticDir
279 280 281 282 283 284 285
      ? [
          {
            // 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*'),
286
            name: 'static catchall',
287 288 289
            fn: async (req, res, params, parsedUrl) => {
              const p = join(this.dir, 'static', ...(params.path || []))
              await this.serveStatic(req, res, p, parsedUrl)
290 291 292
              return {
                finished: true,
              }
293 294 295 296
            },
          } as Route,
        ]
      : []
297

298
    const fsRoutes: Route[] = [
T
Tim Neutkens 已提交
299
      {
300
        match: route('/_next/static/:path*'),
301 302
        type: 'route',
        name: '_next/static catchall',
303
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
304 305 306
          // 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.
307 308

          // make sure to 404 for /_next/static itself
309 310 311 312 313 314
          if (!params.path) {
            await this.render404(req, res, parsedUrl)
            return {
              finished: true,
            }
          }
315

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

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

J
JJ Kasper 已提交
363 364 365 366 367 368 369 370 371
          req.url = pathname
          const parsedUrl = parseUrl(pathname, true)
          await this.render(
            req,
            res,
            pathname,
            { _nextSprData: '1' },
            parsedUrl
          )
372 373 374
          return {
            finished: true,
          }
J
JJ Kasper 已提交
375 376
        },
      },
T
Tim Neutkens 已提交
377
      {
378
        match: route('/_next/:path*'),
379 380
        type: 'route',
        name: '_next catchall',
T
Tim Neutkens 已提交
381
        // This path is needed because `render()` does a check for `/_next` and the calls the routing again
382
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
383
          await this.render404(req, res, parsedUrl)
384 385 386
          return {
            finished: true,
          }
L
Lukáš Huvar 已提交
387 388
        },
      },
389 390
      ...publicRoutes,
      ...staticFilesRoute,
T
Tim Neutkens 已提交
391
    ]
392
    const routes: Route[] = []
393

J
JJ Kasper 已提交
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411
    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(
412
        ...customRoutes.map(route => {
J
JJ Kasper 已提交
413
          return {
414
            check: true,
415 416 417 418 419
            match: route.matcher,
            type: route.type,
            statusCode: route.statusCode,
            name: `${route.type} ${route.source} route`,
            fn: async (_req, res, params, _parsedUrl) => {
420 421 422 423
              const parsedDestination = parseUrl(route.destination, true)
              let destinationCompiler = compilePathToRegex(
                `${parsedDestination.pathname!}${parsedDestination.hash || ''}`
              )
424 425 426 427 428 429
              let newUrl

              try {
                newUrl = destinationCompiler(params)
              } catch (err) {
                if (
J
Joe Haddad 已提交
430 431 432
                  err.message.match(
                    /Expected .*? to not repeat, but got an array/
                  )
433 434 435 436 437 438 439
                ) {
                  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
              }
440 441

              if (route.type === 'redirect') {
442 443 444 445 446 447 448 449 450
                const parsedNewUrl = parseUrl(newUrl)
                res.setHeader(
                  'Location',
                  formatUrl({
                    ...parsedDestination,
                    pathname: parsedNewUrl.pathname,
                    hash: parsedNewUrl.hash,
                  })
                )
451 452 453 454
                res.statusCode = route.statusCode || DEFAULT_REDIRECT_STATUS
                res.end()
                return {
                  finished: true,
J
JJ Kasper 已提交
455 456 457
                }
              }

458 459 460
              return {
                finished: false,
                pathname: newUrl,
J
JJ Kasper 已提交
461 462 463 464 465 466 467
              }
            },
          } as Route
        })
      )
    }

468 469
    const catchAllRoute: Route = {
      match: route('/:path*'),
470
      type: 'route',
471
      name: 'Catchall render',
472
      fn: async (req, res, params, parsedUrl) => {
473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496
        const { pathname, query } = parsedUrl
        if (!pathname) {
          throw new Error('pathname is undefined')
        }

        // Used in development to check public directory paths
        if (await this._beforeCatchAllRender(req, res, params, parsedUrl)) {
          return {
            finished: true,
          }
        }

        if (params && params.path && params.path[0] === 'api') {
          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)
497 498 499 500
        return {
          finished: true,
        }
      },
501
    }
502

503
    if (this.nextConfig.useFileSystemPublicRoutes) {
J
Joe Haddad 已提交
504
      this.dynamicRoutes = this.getDynamicRoutes()
J
Joe Haddad 已提交
505

506 507 508
      // 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.
509
      // See more: https://github.com/zeit/next.js/issues/2617
510
      routes.push(catchAllRoute)
511
    }
N
nkzawa 已提交
512

513 514 515 516 517 518 519
    return {
      routes,
      fsRoutes,
      catchAllRoute,
      dynamicRoutes: this.dynamicRoutes,
      pageChecker: this.hasPage.bind(this),
    }
T
Tim Neutkens 已提交
520 521
  }

522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539
  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
  }

540 541 542 543 544 545 546 547 548
  protected async _beforeCatchAllRender(
    _req: IncomingMessage,
    _res: ServerResponse,
    _params: Params,
    _parsedUrl: UrlWithParsedQuery
  ) {
    return false
  }

549 550 551
  // Used to build API page in development
  protected async ensureApiPage(pathname: string) {}

L
Lukáš Huvar 已提交
552 553 554 555 556 557
  /**
   * Resolves `API` request, in development builds on demand
   * @param req http request
   * @param res http response
   * @param pathname path of request
   */
J
Joe Haddad 已提交
558
  private async handleApiRequest(
559 560
    req: IncomingMessage,
    res: ServerResponse,
561
    pathname: string
J
Joe Haddad 已提交
562
  ) {
563
    let page = pathname
L
Lukáš Huvar 已提交
564
    let params: Params | boolean = false
565
    let pageFound = await this.hasPage(page)
J
JJ Kasper 已提交
566

567
    if (!pageFound && this.dynamicRoutes) {
L
Lukáš Huvar 已提交
568 569 570
      for (const dynamicRoute of this.dynamicRoutes) {
        params = dynamicRoute.match(pathname)
        if (params) {
571 572
          page = dynamicRoute.page
          pageFound = true
L
Lukáš Huvar 已提交
573 574 575 576 577
          break
        }
      }
    }

578
    if (!pageFound) {
579
      return false
J
JJ Kasper 已提交
580
    }
581 582 583 584 585 586
    // 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 已提交
587

588
    if (!this.renderOpts.dev && this._isLikeServerless) {
589
      if (typeof pageModule.default === 'function') {
590 591
        await pageModule.default(req, res)
        return true
J
JJ Kasper 已提交
592 593 594
      }
    }

595
    await apiResolver(req, res, params, pageModule, this.onErrorMiddleware)
596
    return true
L
Lukáš Huvar 已提交
597 598
  }

599
  protected generatePublicRoutes(): Route[] {
600 601 602
    const routes: Route[] = []
    const publicFiles = recursiveReadDirSync(this.publicDir)

603
    publicFiles.forEach(path => {
604 605
      const unixPath = path.replace(/\\/g, '/')
      // Only include public files that will not replace a page path
606 607
      // this should not occur now that we check this during build
      if (!this.pagesManifest![unixPath]) {
608 609
        routes.push({
          match: route(unixPath),
610 611
          type: 'route',
          name: 'public catchall',
612 613 614
          fn: async (req, res, _params, parsedUrl) => {
            const p = join(this.publicDir, unixPath)
            await this.serveStatic(req, res, p, parsedUrl)
615 616 617
            return {
              finished: true,
            }
618 619 620 621 622 623 624 625
          },
        })
      }
    })

    return routes
  }

626
  protected getDynamicRoutes() {
627 628 629
    const dynamicRoutedPages = Object.keys(this.pagesManifest!).filter(
      isDynamicRoute
    )
630 631 632 633
    return getSortedRoutes(dynamicRoutedPages).map(page => ({
      page,
      match: getRouteMatcher(getRouteRegex(page)),
    }))
J
Joe Haddad 已提交
634 635
  }

636 637 638 639 640 641
  private handleCompression(req: IncomingMessage, res: ServerResponse) {
    if (this.compression) {
      this.compression(req, res, () => {})
    }
  }

642
  protected async run(
J
Joe Haddad 已提交
643 644
    req: IncomingMessage,
    res: ServerResponse,
645
    parsedUrl: UrlWithParsedQuery
J
Joe Haddad 已提交
646
  ) {
647 648
    this.handleCompression(req, res)

649
    try {
650 651
      const matched = await this.router.execute(req, res, parsedUrl)
      if (matched) {
652 653 654 655 656 657 658 659
        return
      }
    } catch (err) {
      if (err.code === 'DECODE_FAILED') {
        res.statusCode = 400
        return this.renderError(null, req, res, '/_error', {})
      }
      throw err
660 661
    }

662
    await this.render404(req, res, parsedUrl)
N
nkzawa 已提交
663 664
  }

665
  protected async sendHTML(
J
Joe Haddad 已提交
666 667
    req: IncomingMessage,
    res: ServerResponse,
668
    html: string
J
Joe Haddad 已提交
669
  ) {
T
Tim Neutkens 已提交
670 671
    const { generateEtags, poweredByHeader } = this.renderOpts
    return sendHTML(req, res, html, { generateEtags, poweredByHeader })
672 673
  }

J
Joe Haddad 已提交
674 675 676 677 678
  public async render(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
679
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
680
  ): Promise<void> {
681
    const url: any = req.url
682 683 684 685 686

    if (
      url.match(/^\/_next\//) ||
      (this.hasStaticDir && url.match(/^\/static\//))
    ) {
687 688 689
      return this.handleRequest(req, res, parsedUrl)
    }

690
    if (isBlockedPage(pathname)) {
691
      return this.render404(req, res, parsedUrl)
692 693
    }

694
    const html = await this.renderToHTML(req, res, pathname, query, {
J
Joe Haddad 已提交
695 696 697 698 699
      dataOnly:
        (this.renderOpts.ampBindInitData && Boolean(query.dataOnly)) ||
        (req.headers &&
          (req.headers.accept || '').indexOf('application/amp.bind+json') !==
            -1),
700
    })
701 702
    // Request was ended by the user
    if (html === null) {
703 704 705
      return
    }

706
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
707
  }
N
nkzawa 已提交
708

J
Joe Haddad 已提交
709
  private async findPageComponents(
J
Joe Haddad 已提交
710
    pathname: string,
711
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
712
  ) {
713
    const serverless = !this.renderOpts.dev && this._isLikeServerless
J
JJ Kasper 已提交
714 715 716
    // try serving a static AMP version first
    if (query.amp) {
      try {
J
Joe Haddad 已提交
717 718 719 720
        return await loadComponents(
          this.distDir,
          this.buildId,
          (pathname === '/' ? '/index' : pathname) + '.amp',
721
          serverless
J
Joe Haddad 已提交
722
        )
J
JJ Kasper 已提交
723 724 725 726
      } catch (err) {
        if (err.code !== 'ENOENT') throw err
      }
    }
J
Joe Haddad 已提交
727 728 729 730
    return await loadComponents(
      this.distDir,
      this.buildId,
      pathname,
731
      serverless
J
Joe Haddad 已提交
732 733 734
    )
  }

J
JJ Kasper 已提交
735 736 737 738 739 740
  private __sendPayload(
    res: ServerResponse,
    payload: any,
    type: string,
    revalidate?: number | false
  ) {
J
JJ Kasper 已提交
741
    // TODO: ETag? Cache-Control headers? Next-specific headers?
J
JJ Kasper 已提交
742
    res.setHeader('Content-Type', type)
J
JJ Kasper 已提交
743
    res.setHeader('Content-Length', Buffer.byteLength(payload))
J
Joe Haddad 已提交
744 745 746 747 748 749 750 751 752 753 754 755
    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 已提交
756
    }
J
JJ Kasper 已提交
757 758 759
    res.end(payload)
  }

J
Joe Haddad 已提交
760 761 762 763 764 765
  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
    result: LoadComponentsReturnType,
766
    opts: any
J
JJ Kasper 已提交
767
  ): Promise<string | null> {
J
JJ Kasper 已提交
768
    // handle static page
J
Joe Haddad 已提交
769 770 771 772
    if (typeof result.Component === 'string') {
      return result.Component
    }

J
JJ Kasper 已提交
773 774
    // check request state
    const isLikeServerless =
J
Joe Haddad 已提交
775
      typeof result.Component === 'object' &&
J
JJ Kasper 已提交
776
      typeof result.Component.renderReqToHTML === 'function'
J
JJ Kasper 已提交
777 778 779 780 781 782
    const isSpr = !!result.unstable_getStaticProps

    // non-spr requests should render like normal
    if (!isSpr) {
      // handle serverless
      if (isLikeServerless) {
783 784 785 786 787 788 789 790
        const curUrl = parseUrl(req.url!, true)
        req.url = formatUrl({
          ...curUrl,
          query: {
            ...curUrl.query,
            ...query,
          },
        })
J
JJ Kasper 已提交
791 792 793 794 795 796 797 798 799 800 801
        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
802 803
    delete query._nextSprData

J
JJ Kasper 已提交
804 805 806 807 808 809 810 811 812 813 814 815 816
    // 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 已提交
817 818
        isSprData ? 'application/json' : 'text/html; charset=utf-8',
        cachedData.curRevalidate
J
JJ Kasper 已提交
819 820 821 822 823 824
      )

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

J
JJ Kasper 已提交
827 828 829 830 831
    // 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 已提交
832 833 834
      let { pathname } = parseUrl(req.url || '', true)
      pathname = !pathname || pathname === '/' ? '/index' : pathname
      req.url = `/_next/data/${this.buildId}${pathname}.json`
J
JJ Kasper 已提交
835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866
    }

    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 }
867
    })
J
JJ Kasper 已提交
868 869 870 871 872 873 874 875

    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 已提交
876 877
            isSprData ? 'application/json' : 'text/html; charset=utf-8',
            sprRevalidate
J
JJ Kasper 已提交
878 879 880 881 882 883 884 885 886 887 888 889 890 891 892
          )
        }

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

        return null
      }
    )
893 894
  }

J
Joe Haddad 已提交
895
  public renderToHTML(
J
Joe Haddad 已提交
896 897 898 899
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
J
Joe Haddad 已提交
900 901 902 903 904 905 906
    {
      amphtml,
      dataOnly,
      hasAmp,
    }: {
      amphtml?: boolean
      hasAmp?: boolean
907 908
      dataOnly?: boolean
    } = {}
J
Joe Haddad 已提交
909
  ): Promise<string | null> {
J
Joe Haddad 已提交
910 911
    return this.findPageComponents(pathname, query)
      .then(
912
        result => {
J
Joe Haddad 已提交
913 914 915 916
          return this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
917 918 919
            result.unstable_getStaticProps
              ? { _nextSprData: query._nextSprData }
              : query,
J
Joe Haddad 已提交
920
            result,
921
            { ...this.renderOpts, amphtml, hasAmp, dataOnly }
J
Joe Haddad 已提交
922 923
          )
        },
924
        err => {
J
Joe Haddad 已提交
925 926 927 928 929 930 931 932 933 934 935
          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(
936 937
              result => {
                return this.renderToHTMLWithComponents(
J
Joe Haddad 已提交
938 939 940
                  req,
                  res,
                  dynamicRoute.page,
J
JJ Kasper 已提交
941 942 943 944 945 946 947
                  // only add params for SPR enabled pages
                  {
                    ...(result.unstable_getStaticProps
                      ? { _nextSprData: query._nextSprData }
                      : query),
                    ...params,
                  },
J
Joe Haddad 已提交
948
                  result,
J
JJ Kasper 已提交
949 950 951 952 953 954
                  {
                    ...this.renderOpts,
                    amphtml,
                    hasAmp,
                    dataOnly,
                  }
955
                )
956
              }
J
Joe Haddad 已提交
957 958 959 960
            )
          }

          return Promise.reject(err)
961
        }
J
Joe Haddad 已提交
962
      )
963
      .catch(err => {
J
Joe Haddad 已提交
964 965 966 967 968 969 970 971 972
        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 已提交
973 974
  }

J
Joe Haddad 已提交
975 976 977 978 979
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
980
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
981 982 983
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
984
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
985
    )
N
Naoyuki Kanezawa 已提交
986
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
987
    if (html === null) {
988 989
      return
    }
990
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
991 992
  }

J
Joe Haddad 已提交
993 994 995 996 997
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
998
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
999
  ) {
J
Joe Haddad 已提交
1000
    const result = await this.findPageComponents('/_error', query)
1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019
    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 已提交
1020 1021
  }

J
Joe Haddad 已提交
1022 1023 1024
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
1025
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1026
  ): Promise<void> {
1027 1028
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
1029
    if (!pathname) {
1030 1031
      throw new Error('pathname is undefined')
    }
N
Naoyuki Kanezawa 已提交
1032
    res.statusCode = 404
1033
    return this.renderError(null, req, res, pathname, query)
N
Naoyuki Kanezawa 已提交
1034
  }
N
Naoyuki Kanezawa 已提交
1035

J
Joe Haddad 已提交
1036 1037 1038 1039
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
1040
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
1041
  ): Promise<void> {
A
Arunoda Susiripala 已提交
1042
    if (!this.isServeableUrl(path)) {
1043
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
1044 1045
    }

1046 1047 1048 1049 1050 1051
    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 已提交
1052
    try {
1053
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
1054
    } catch (err) {
T
Tim Neutkens 已提交
1055
      if (err.code === 'ENOENT' || err.statusCode === 404) {
1056
        this.render404(req, res, parsedUrl)
1057 1058 1059
      } else if (err.statusCode === 412) {
        res.statusCode = 412
        return this.renderError(err, req, res, path)
N
Naoyuki Kanezawa 已提交
1060 1061 1062 1063 1064 1065
      } else {
        throw err
      }
    }
  }

1066
  private isServeableUrl(path: string): boolean {
A
Arunoda Susiripala 已提交
1067 1068
    const resolved = resolve(path)
    if (
1069
      resolved.indexOf(join(this.distDir) + sep) !== 0 &&
1070 1071
      resolved.indexOf(join(this.dir, 'static') + sep) !== 0 &&
      resolved.indexOf(join(this.dir, 'public') + sep) !== 0
A
Arunoda Susiripala 已提交
1072 1073 1074 1075 1076 1077 1078 1079
    ) {
      // Seems like the user is trying to traverse the filesystem.
      return false
    }

    return true
  }

1080
  protected readBuildId(): string {
1081 1082 1083 1084 1085
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
1086
        throw new Error(
1087
          `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 已提交
1088
        )
1089 1090 1091
      }

      throw err
1092
    }
1093
  }
1094 1095 1096 1097

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