next-server.ts 16.4 KB
Newer Older
1
/* eslint-disable import/first */
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'
5
import { parse as parseQs, ParsedUrlQuery } from 'querystring'
J
Joe Haddad 已提交
6 7
import { parse as parseUrl, UrlWithParsedQuery } from 'url'

J
Joe Haddad 已提交
8 9
import {
  BUILD_ID_FILE,
J
Joe Haddad 已提交
10
  BUILD_MANIFEST,
11
  CLIENT_PUBLIC_FILES_PATH,
J
Joe Haddad 已提交
12 13
  CLIENT_STATIC_FILES_PATH,
  CLIENT_STATIC_FILES_RUNTIME,
14
  PAGES_MANIFEST,
J
Joe Haddad 已提交
15 16
  PHASE_PRODUCTION_SERVER,
  SERVER_DIRECTORY,
T
Tim Neutkens 已提交
17
} from '../lib/constants'
J
Joe Haddad 已提交
18
import { getRouteMatch } from '../lib/router/utils'
19
import * as envConfig from '../lib/runtime-config'
J
Joe Haddad 已提交
20 21
import loadConfig from './config'
import { recursiveReadDirSync } from './lib/recursive-readdir-sync'
J
Joe Haddad 已提交
22 23
import {
  interopDefault,
J
Joe Haddad 已提交
24
  loadComponents,
J
Joe Haddad 已提交
25 26
  LoadComponentsReturnType,
} from './load-components'
J
Joe Haddad 已提交
27
import { renderToHTML } from './render'
J
Joe Haddad 已提交
28
import { getPagePath } from './require'
J
Joe Haddad 已提交
29 30 31 32
import Router, { route, Route, RouteMatch } from './router'
import { sendHTML } from './send-html'
import { serveStatic } from './serve-static'
import { isBlockedPage, isInternalUrl } from './utils'
33 34 35

type NextConfig = any

T
Tim Neutkens 已提交
36
export type ServerConstructor = {
J
Joe Haddad 已提交
37 38 39
  dir?: string
  staticMarkup?: boolean
  quiet?: boolean
40
  conf?: NextConfig
41
}
42

N
nkzawa 已提交
43
export default class Server {
44 45 46 47
  dir: string
  quiet: boolean
  nextConfig: NextConfig
  distDir: string
48
  publicDir: string
J
Joe Haddad 已提交
49
  buildManifest: string
50 51
  buildId: string
  renderOpts: {
T
Tim Neutkens 已提交
52
    poweredByHeader: boolean
T
Tim Neutkens 已提交
53
    ampBindInitData: boolean
J
Joe Haddad 已提交
54 55 56 57
    staticMarkup: boolean
    buildId: string
    generateEtags: boolean
    runtimeConfig?: { [key: string]: any }
58 59
    assetPrefix?: string
    canonicalBase: string
J
Joe Haddad 已提交
60
    autoExport: boolean
61
    dev?: boolean
62 63
  }
  router: Router
J
Joe Haddad 已提交
64
  private dynamicRoutes?: Array<{ page: string; match: RouteMatch }>
65

J
Joe Haddad 已提交
66 67 68 69 70 71
  public constructor({
    dir = '.',
    staticMarkup = false,
    quiet = false,
    conf = null,
  }: ServerConstructor = {}) {
N
nkzawa 已提交
72
    this.dir = resolve(dir)
N
Naoyuki Kanezawa 已提交
73
    this.quiet = quiet
T
Tim Neutkens 已提交
74
    const phase = this.currentPhase()
75
    this.nextConfig = loadConfig(phase, this.dir, conf)
76
    this.distDir = join(this.dir, this.nextConfig.distDir)
77 78
    // this.pagesDir = join(this.dir, 'pages')
    this.publicDir = join(this.dir, CLIENT_PUBLIC_FILES_PATH)
J
Joe Haddad 已提交
79
    this.buildManifest = join(this.distDir, BUILD_MANIFEST)
T
Tim Neutkens 已提交
80

81 82
    // Only serverRuntimeConfig needs the default
    // publicRuntimeConfig gets it's default in client/index.js
J
Joe Haddad 已提交
83 84 85 86 87 88
    const {
      serverRuntimeConfig = {},
      publicRuntimeConfig,
      assetPrefix,
      generateEtags,
    } = this.nextConfig
89

T
Tim Neutkens 已提交
90
    this.buildId = this.readBuildId()
91

92
    this.renderOpts = {
T
Tim Neutkens 已提交
93
      ampBindInitData: this.nextConfig.experimental.ampBindInitData,
T
Tim Neutkens 已提交
94
      poweredByHeader: this.nextConfig.poweredByHeader,
95
      canonicalBase: this.nextConfig.amp.canonicalBase,
96
      autoExport: this.nextConfig.experimental.autoExport,
97
      staticMarkup,
98
      buildId: this.buildId,
99
      generateEtags,
100
    }
N
Naoyuki Kanezawa 已提交
101

102 103 104 105
    // Only the `publicRuntimeConfig` key is exposed to the client side
    // It'll be rendered as part of __NEXT_DATA__ on the client side
    if (publicRuntimeConfig) {
      this.renderOpts.runtimeConfig = publicRuntimeConfig
106 107
    }

108 109 110
    // Initialize next/config with the environment configuration
    envConfig.setConfig({
      serverRuntimeConfig,
111
      publicRuntimeConfig,
112 113
    })

114 115
    const routes = this.generateRoutes()
    this.router = new Router(routes)
116

117
    this.setAssetPrefix(assetPrefix)
N
Naoyuki Kanezawa 已提交
118
  }
N
nkzawa 已提交
119

120
  private currentPhase(): string {
121
    return PHASE_PRODUCTION_SERVER
122 123
  }

124
  private logError(...args: any): void {
125 126
    if (this.quiet) return
    // tslint:disable-next-line
127 128 129
    console.error(...args)
  }

J
Joe Haddad 已提交
130 131 132
  private handleRequest(
    req: IncomingMessage,
    res: ServerResponse,
133
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
134
  ): Promise<void> {
135
    // Parse url if parsedUrl not provided
136
    if (!parsedUrl || typeof parsedUrl !== 'object') {
137 138
      const url: any = req.url
      parsedUrl = parseUrl(url, true)
139
    }
140

141 142 143
    // 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 已提交
144
    }
145

146
    res.statusCode = 200
147
    return this.run(req, res, parsedUrl).catch(err => {
J
Joe Haddad 已提交
148 149 150 151
      this.logError(err)
      res.statusCode = 500
      res.end('Internal Server Error')
    })
152 153
  }

154
  public getRequestHandler() {
155
    return this.handleRequest.bind(this)
N
nkzawa 已提交
156 157
  }

158
  public setAssetPrefix(prefix?: string) {
159
    this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
160 161
  }

162
  // Backwards compatibility
163
  public async prepare(): Promise<void> {}
N
nkzawa 已提交
164

T
Tim Neutkens 已提交
165
  // Backwards compatibility
166
  private async close(): Promise<void> {}
T
Tim Neutkens 已提交
167

168
  private setImmutableAssetCacheControl(res: ServerResponse) {
T
Tim Neutkens 已提交
169
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
N
nkzawa 已提交
170 171
  }

172
  private generateRoutes(): Route[] {
173
    const routes: Route[] = [
T
Tim Neutkens 已提交
174
      {
175
        match: route('/_next/static/:path*'),
176
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
177 178 179
          // 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.
J
Joe Haddad 已提交
180 181 182 183 184
          if (
            params.path[0] === CLIENT_STATIC_FILES_RUNTIME ||
            params.path[0] === 'chunks' ||
            params.path[0] === this.buildId
          ) {
T
Tim Neutkens 已提交
185
            this.setImmutableAssetCacheControl(res)
186
          }
J
Joe Haddad 已提交
187 188 189
          const p = join(
            this.distDir,
            CLIENT_STATIC_FILES_PATH,
190
            ...(params.path || [])
J
Joe Haddad 已提交
191
          )
192
          await this.serveStatic(req, res, p, parsedUrl)
193
        },
194
      },
T
Tim Neutkens 已提交
195
      {
196
        match: route('/_next/:path*'),
T
Tim Neutkens 已提交
197
        // This path is needed because `render()` does a check for `/_next` and the calls the routing again
198
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
199
          await this.render404(req, res, parsedUrl)
200
        },
T
Tim Neutkens 已提交
201 202
      },
      {
203 204 205
        // 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.
T
Tim Neutkens 已提交
206
        // See more: https://github.com/zeit/next.js/issues/2617
207
        match: route('/static/:path*'),
208
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
209
          const p = join(this.dir, 'static', ...(params.path || []))
210
          await this.serveStatic(req, res, p, parsedUrl)
211 212
        },
      },
L
Lukáš Huvar 已提交
213 214 215 216 217 218 219
      {
        match: route('/api/:path*'),
        fn: async (req, res, params, parsedUrl) => {
          const { pathname } = parsedUrl
          await this.handleApiRequest(req, res, pathname!)
        },
      },
T
Tim Neutkens 已提交
220
    ]
221

222 223 224 225
    if (fs.existsSync(this.publicDir)) {
      routes.push(...this.generatePublicRoutes())
    }

226
    if (this.nextConfig.useFileSystemPublicRoutes) {
J
Joe Haddad 已提交
227 228 229 230
      this.dynamicRoutes = this.nextConfig.experimental.dynamicRouting
        ? this.getDynamicRoutes()
        : []

231 232 233
      // 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.
234
      // See more: https://github.com/zeit/next.js/issues/2617
T
Tim Neutkens 已提交
235
      routes.push({
236
        match: route('/:path*'),
237
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
238
          const { pathname, query } = parsedUrl
239
          if (!pathname) {
240 241
            throw new Error('pathname is undefined')
          }
242

243
          await this.render(req, res, pathname, query, parsedUrl)
244
        },
T
Tim Neutkens 已提交
245
      })
246
    }
N
nkzawa 已提交
247

T
Tim Neutkens 已提交
248 249 250
    return routes
  }

L
Lukáš Huvar 已提交
251 252 253 254 255 256
  /**
   * Resolves `API` request, in development builds on demand
   * @param req http request
   * @param res http response
   * @param pathname path of request
   */
J
Joe Haddad 已提交
257 258 259
  private async handleApiRequest(
    req: IncomingMessage,
    res: ServerResponse,
260
    pathname: string
J
Joe Haddad 已提交
261
  ) {
L
Lukáš Huvar 已提交
262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277
    const resolverFunction = await this.resolveApiRequest(pathname)
    if (resolverFunction === null) {
      res.statusCode = 404
      res.end('Not Found')
      return
    }

    const resolver = interopDefault(require(resolverFunction))
    resolver(req, res)
  }

  /**
   * Resolves path to resolver function
   * @param pathname path of request
   */
  private resolveApiRequest(pathname: string) {
J
Joe Haddad 已提交
278 279 280
    return getPagePath(
      pathname,
      this.distDir,
281
      this.nextConfig.target === 'serverless'
J
Joe Haddad 已提交
282
    )
L
Lukáš Huvar 已提交
283 284
  }

285 286 287 288 289 290
  private generatePublicRoutes(): Route[] {
    const routes: Route[] = []
    const publicFiles = recursiveReadDirSync(this.publicDir)
    const serverBuildPath = join(this.distDir, SERVER_DIRECTORY)
    const pagesManifest = require(join(serverBuildPath, PAGES_MANIFEST))

291
    publicFiles.forEach(path => {
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307
      const unixPath = path.replace(/\\/g, '/')
      // Only include public files that will not replace a page path
      if (!pagesManifest[unixPath]) {
        routes.push({
          match: route(unixPath),
          fn: async (req, res, _params, parsedUrl) => {
            const p = join(this.publicDir, unixPath)
            await this.serveStatic(req, res, p, parsedUrl)
          },
        })
      }
    })

    return routes
  }

J
Joe Haddad 已提交
308 309
  private getDynamicRoutes() {
    const manifest = require(this.buildManifest)
310 311
    const dynamicRoutedPages = Object.keys(manifest.pages).filter(p =>
      p.includes('/$')
J
Joe Haddad 已提交
312 313
    )
    return dynamicRoutedPages
314
      .map(page => ({
J
Joe Haddad 已提交
315 316 317 318
        page,
        match: getRouteMatch(page),
      }))
      .sort((a, b) =>
319
        Math.sign(a.page.match(/\/\$/g)!.length - b.page.match(/\/\$/g)!.length)
J
Joe Haddad 已提交
320 321 322
      )
  }

J
Joe Haddad 已提交
323 324 325
  private async run(
    req: IncomingMessage,
    res: ServerResponse,
326
    parsedUrl: UrlWithParsedQuery
J
Joe Haddad 已提交
327
  ) {
328 329 330 331 332 333 334 335 336 337 338 339
    try {
      const fn = this.router.match(req, res, parsedUrl)
      if (fn) {
        await fn()
        return
      }
    } catch (err) {
      if (err.code === 'DECODE_FAILED') {
        res.statusCode = 400
        return this.renderError(null, req, res, '/_error', {})
      }
      throw err
340 341 342
    }

    if (req.method === 'GET' || req.method === 'HEAD') {
343
      await this.render404(req, res, parsedUrl)
344 345
    } else {
      res.statusCode = 501
346
      res.end('Not Implemented')
N
nkzawa 已提交
347 348 349
    }
  }

J
Joe Haddad 已提交
350 351 352
  private async sendHTML(
    req: IncomingMessage,
    res: ServerResponse,
353
    html: string
J
Joe Haddad 已提交
354
  ) {
T
Tim Neutkens 已提交
355 356
    const { generateEtags, poweredByHeader } = this.renderOpts
    return sendHTML(req, res, html, { generateEtags, poweredByHeader })
357 358
  }

J
Joe Haddad 已提交
359 360 361 362 363
  public async render(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
364
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
365
  ): Promise<void> {
366 367
    const url: any = req.url
    if (isInternalUrl(url)) {
368 369 370
      return this.handleRequest(req, res, parsedUrl)
    }

371
    if (isBlockedPage(pathname)) {
372
      return this.render404(req, res, parsedUrl)
373 374
    }

375
    const html = await this.renderToHTML(req, res, pathname, query, {
J
Joe Haddad 已提交
376 377 378 379 380
      dataOnly:
        (this.renderOpts.ampBindInitData && Boolean(query.dataOnly)) ||
        (req.headers &&
          (req.headers.accept || '').indexOf('application/amp.bind+json') !==
            -1),
381
    })
382 383
    // Request was ended by the user
    if (html === null) {
384 385 386
      return
    }

387
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
388
  }
N
nkzawa 已提交
389

J
Joe Haddad 已提交
390
  private async findPageComponents(
J
Joe Haddad 已提交
391
    pathname: string,
392
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
393
  ) {
J
Joe Haddad 已提交
394 395
    const serverless =
      !this.renderOpts.dev && this.nextConfig.target === 'serverless'
J
JJ Kasper 已提交
396 397 398
    // try serving a static AMP version first
    if (query.amp) {
      try {
J
Joe Haddad 已提交
399 400 401 402
        return await loadComponents(
          this.distDir,
          this.buildId,
          (pathname === '/' ? '/index' : pathname) + '.amp',
403
          serverless
J
Joe Haddad 已提交
404
        )
J
JJ Kasper 已提交
405 406 407 408
      } catch (err) {
        if (err.code !== 'ENOENT') throw err
      }
    }
J
Joe Haddad 已提交
409 410 411 412
    return await loadComponents(
      this.distDir,
      this.buildId,
      pathname,
413
      serverless
J
Joe Haddad 已提交
414 415 416 417 418 419 420 421 422
    )
  }

  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
    result: LoadComponentsReturnType,
423
    opts: any
J
Joe Haddad 已提交
424
  ) {
J
JJ Kasper 已提交
425
    // handle static page
J
Joe Haddad 已提交
426 427 428 429
    if (typeof result.Component === 'string') {
      return result.Component
    }

J
JJ Kasper 已提交
430
    // handle serverless
J
Joe Haddad 已提交
431 432
    if (
      typeof result.Component === 'object' &&
J
JJ Kasper 已提交
433 434 435 436
      typeof result.Component.renderReqToHTML === 'function'
    ) {
      return result.Component.renderReqToHTML(req, res)
    }
J
Joe Haddad 已提交
437

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

J
Joe Haddad 已提交
441
  public renderToHTML(
J
Joe Haddad 已提交
442 443 444 445
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
J
Joe Haddad 已提交
446 447 448 449 450 451 452
    {
      amphtml,
      dataOnly,
      hasAmp,
    }: {
      amphtml?: boolean
      hasAmp?: boolean
453 454
      dataOnly?: boolean
    } = {}
J
Joe Haddad 已提交
455
  ): Promise<string | null> {
J
Joe Haddad 已提交
456 457
    return this.findPageComponents(pathname, query)
      .then(
458
        result => {
J
Joe Haddad 已提交
459 460 461 462 463 464
          return this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
            query,
            result,
465
            { ...this.renderOpts, amphtml, hasAmp, dataOnly }
J
Joe Haddad 已提交
466 467
          )
        },
468
        err => {
J
Joe Haddad 已提交
469 470 471 472 473 474 475 476 477 478 479
          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(
480
              result =>
J
Joe Haddad 已提交
481 482 483 484 485 486
                this.renderToHTMLWithComponents(
                  req,
                  res,
                  dynamicRoute.page,
                  { ...query, ...params },
                  result,
487 488
                  { ...this.renderOpts, amphtml, hasAmp, dataOnly }
                )
J
Joe Haddad 已提交
489 490 491 492
            )
          }

          return Promise.reject(err)
493
        }
J
Joe Haddad 已提交
494
      )
495
      .catch(err => {
J
Joe Haddad 已提交
496 497 498 499 500 501 502 503 504
        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 已提交
505 506
  }

J
Joe Haddad 已提交
507 508 509 510 511
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
512
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
513 514 515
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
516
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
517
    )
N
Naoyuki Kanezawa 已提交
518
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
519
    if (html === null) {
520 521
      return
    }
522
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
523 524
  }

J
Joe Haddad 已提交
525 526 527 528 529
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
530
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
531
  ) {
J
Joe Haddad 已提交
532 533
    const result = await this.findPageComponents('/_error', query)
    return this.renderToHTMLWithComponents(req, res, '/_error', query, result, {
J
Joe Haddad 已提交
534 535 536
      ...this.renderOpts,
      err,
    })
N
Naoyuki Kanezawa 已提交
537 538
  }

J
Joe Haddad 已提交
539 540 541
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
542
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
543
  ): Promise<void> {
544 545
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
546
    if (!pathname) {
547 548
      throw new Error('pathname is undefined')
    }
N
Naoyuki Kanezawa 已提交
549
    res.statusCode = 404
550
    return this.renderError(null, req, res, pathname, query)
N
Naoyuki Kanezawa 已提交
551
  }
N
Naoyuki Kanezawa 已提交
552

J
Joe Haddad 已提交
553 554 555 556
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
557
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
558
  ): Promise<void> {
A
Arunoda Susiripala 已提交
559
    if (!this.isServeableUrl(path)) {
560
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
561 562
    }

N
Naoyuki Kanezawa 已提交
563
    try {
564
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
565
    } catch (err) {
T
Tim Neutkens 已提交
566
      if (err.code === 'ENOENT' || err.statusCode === 404) {
567
        this.render404(req, res, parsedUrl)
N
Naoyuki Kanezawa 已提交
568 569 570 571 572 573
      } else {
        throw err
      }
    }
  }

574
  private isServeableUrl(path: string): boolean {
A
Arunoda Susiripala 已提交
575 576
    const resolved = resolve(path)
    if (
577
      resolved.indexOf(join(this.distDir) + sep) !== 0 &&
578 579
      resolved.indexOf(join(this.dir, 'static') + sep) !== 0 &&
      resolved.indexOf(join(this.dir, 'public') + sep) !== 0
A
Arunoda Susiripala 已提交
580 581 582 583 584 585 586 587
    ) {
      // Seems like the user is trying to traverse the filesystem.
      return false
    }

    return true
  }

588
  private readBuildId(): string {
589 590 591 592 593
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
594 595 596
        throw new Error(
          `Could not find a valid build in the '${
            this.distDir
597
          }' directory! Try building your app with 'next build' before starting the server.`
J
Joe Haddad 已提交
598
        )
599 600 601
      }

      throw err
602
    }
603
  }
604
}