next-server.ts 17.8 KB
Newer Older
J
Joe Haddad 已提交
1
import fs from 'fs'
J
Joe Haddad 已提交
2
import { IncomingMessage, ServerResponse } from 'http'
J
Joe Haddad 已提交
3
import { join, resolve, sep } from 'path'
4
import { parse as parseQs, ParsedUrlQuery } from 'querystring'
J
Joe Haddad 已提交
5 6
import { parse as parseUrl, UrlWithParsedQuery } from 'url'

J
Joe Haddad 已提交
7 8
import {
  BUILD_ID_FILE,
J
Joe Haddad 已提交
9
  BUILD_MANIFEST,
10
  CLIENT_PUBLIC_FILES_PATH,
J
Joe Haddad 已提交
11 12
  CLIENT_STATIC_FILES_PATH,
  CLIENT_STATIC_FILES_RUNTIME,
13
  PAGES_MANIFEST,
J
Joe Haddad 已提交
14 15
  PHASE_PRODUCTION_SERVER,
  SERVER_DIRECTORY,
16
  SERVERLESS_DIRECTORY,
T
Tim Neutkens 已提交
17
} from '../lib/constants'
J
Joe Haddad 已提交
18 19 20 21
import {
  getRouteMatcher,
  getRouteRegex,
  getSortedRoutes,
22
  isDynamicRoute,
J
Joe Haddad 已提交
23
} from '../lib/router/utils'
24
import * as envConfig from '../lib/runtime-config'
L
Lukáš Huvar 已提交
25 26 27 28 29 30 31 32 33 34 35
import { NextApiRequest, NextApiResponse } from '../lib/utils'
import { parse as parseCookies } from 'cookie'
import {
  parseQuery,
  sendJson,
  sendData,
  parseBody,
  sendError,
  ApiError,
  sendStatusCode,
} from './api-utils'
J
Joe Haddad 已提交
36 37
import loadConfig from './config'
import { recursiveReadDirSync } from './lib/recursive-readdir-sync'
J
Joe Haddad 已提交
38 39
import {
  interopDefault,
J
Joe Haddad 已提交
40
  loadComponents,
J
Joe Haddad 已提交
41 42
  LoadComponentsReturnType,
} from './load-components'
J
Joe Haddad 已提交
43
import { renderToHTML } from './render'
J
Joe Haddad 已提交
44
import { getPagePath } from './require'
J
Joe Haddad 已提交
45 46 47 48
import Router, { route, Route, RouteMatch } from './router'
import { sendHTML } from './send-html'
import { serveStatic } from './serve-static'
import { isBlockedPage, isInternalUrl } from './utils'
49 50 51

type NextConfig = any

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

N
nkzawa 已提交
68
export default class Server {
69 70 71 72
  dir: string
  quiet: boolean
  nextConfig: NextConfig
  distDir: string
73
  publicDir: string
J
Joe Haddad 已提交
74
  buildManifest: string
75 76
  buildId: string
  renderOpts: {
T
Tim Neutkens 已提交
77
    poweredByHeader: boolean
T
Tim Neutkens 已提交
78
    ampBindInitData: boolean
J
Joe Haddad 已提交
79 80 81 82
    staticMarkup: boolean
    buildId: string
    generateEtags: boolean
    runtimeConfig?: { [key: string]: any }
83 84
    assetPrefix?: string
    canonicalBase: string
J
Joe Haddad 已提交
85
    autoExport: boolean
86
    dev?: boolean
87 88
  }
  router: Router
J
Joe Haddad 已提交
89
  private dynamicRoutes?: Array<{ page: string; match: RouteMatch }>
90

J
Joe Haddad 已提交
91 92 93 94 95 96
  public constructor({
    dir = '.',
    staticMarkup = false,
    quiet = false,
    conf = null,
  }: ServerConstructor = {}) {
N
nkzawa 已提交
97
    this.dir = resolve(dir)
N
Naoyuki Kanezawa 已提交
98
    this.quiet = quiet
T
Tim Neutkens 已提交
99
    const phase = this.currentPhase()
100
    this.nextConfig = loadConfig(phase, this.dir, conf)
101
    this.distDir = join(this.dir, this.nextConfig.distDir)
102 103
    // this.pagesDir = join(this.dir, 'pages')
    this.publicDir = join(this.dir, CLIENT_PUBLIC_FILES_PATH)
J
Joe Haddad 已提交
104
    this.buildManifest = join(this.distDir, BUILD_MANIFEST)
T
Tim Neutkens 已提交
105

106 107
    // Only serverRuntimeConfig needs the default
    // publicRuntimeConfig gets it's default in client/index.js
J
Joe Haddad 已提交
108 109 110 111 112 113
    const {
      serverRuntimeConfig = {},
      publicRuntimeConfig,
      assetPrefix,
      generateEtags,
    } = this.nextConfig
114

T
Tim Neutkens 已提交
115
    this.buildId = this.readBuildId()
116

117
    this.renderOpts = {
T
Tim Neutkens 已提交
118
      ampBindInitData: this.nextConfig.experimental.ampBindInitData,
T
Tim Neutkens 已提交
119
      poweredByHeader: this.nextConfig.poweredByHeader,
120
      canonicalBase: this.nextConfig.amp.canonicalBase,
121
      autoExport: this.nextConfig.experimental.autoExport,
122
      staticMarkup,
123
      buildId: this.buildId,
124
      generateEtags,
125
    }
N
Naoyuki Kanezawa 已提交
126

127 128 129 130
    // 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
131 132
    }

133 134 135
    // Initialize next/config with the environment configuration
    envConfig.setConfig({
      serverRuntimeConfig,
136
      publicRuntimeConfig,
137 138
    })

139 140
    const routes = this.generateRoutes()
    this.router = new Router(routes)
141

142
    this.setAssetPrefix(assetPrefix)
N
Naoyuki Kanezawa 已提交
143
  }
N
nkzawa 已提交
144

145
  private currentPhase(): string {
146
    return PHASE_PRODUCTION_SERVER
147 148
  }

149
  private logError(...args: any): void {
150 151
    if (this.quiet) return
    // tslint:disable-next-line
152 153 154
    console.error(...args)
  }

J
Joe Haddad 已提交
155 156 157
  private handleRequest(
    req: IncomingMessage,
    res: ServerResponse,
158
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
159
  ): Promise<void> {
160
    // Parse url if parsedUrl not provided
161
    if (!parsedUrl || typeof parsedUrl !== 'object') {
162 163
      const url: any = req.url
      parsedUrl = parseUrl(url, true)
164
    }
165

166 167 168
    // 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 已提交
169
    }
170

171
    res.statusCode = 200
172
    return this.run(req, res, parsedUrl).catch(err => {
J
Joe Haddad 已提交
173 174 175 176
      this.logError(err)
      res.statusCode = 500
      res.end('Internal Server Error')
    })
177 178
  }

179
  public getRequestHandler() {
180
    return this.handleRequest.bind(this)
N
nkzawa 已提交
181 182
  }

183
  public setAssetPrefix(prefix?: string) {
184
    this.renderOpts.assetPrefix = prefix ? prefix.replace(/\/$/, '') : ''
185 186
  }

187
  // Backwards compatibility
188
  public async prepare(): Promise<void> {}
N
nkzawa 已提交
189

T
Tim Neutkens 已提交
190
  // Backwards compatibility
191
  private async close(): Promise<void> {}
T
Tim Neutkens 已提交
192

193
  private setImmutableAssetCacheControl(res: ServerResponse) {
T
Tim Neutkens 已提交
194
    res.setHeader('Cache-Control', 'public, max-age=31536000, immutable')
N
nkzawa 已提交
195 196
  }

197
  private generateRoutes(): Route[] {
198
    const routes: Route[] = [
T
Tim Neutkens 已提交
199
      {
200
        match: route('/_next/static/:path*'),
201
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
202 203 204
          // 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 已提交
205 206 207 208 209
          if (
            params.path[0] === CLIENT_STATIC_FILES_RUNTIME ||
            params.path[0] === 'chunks' ||
            params.path[0] === this.buildId
          ) {
T
Tim Neutkens 已提交
210
            this.setImmutableAssetCacheControl(res)
211
          }
J
Joe Haddad 已提交
212 213 214
          const p = join(
            this.distDir,
            CLIENT_STATIC_FILES_PATH,
215
            ...(params.path || [])
J
Joe Haddad 已提交
216
          )
217
          await this.serveStatic(req, res, p, parsedUrl)
218
        },
219
      },
T
Tim Neutkens 已提交
220
      {
221
        match: route('/_next/:path*'),
T
Tim Neutkens 已提交
222
        // This path is needed because `render()` does a check for `/_next` and the calls the routing again
223
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
224
          await this.render404(req, res, parsedUrl)
225
        },
T
Tim Neutkens 已提交
226 227
      },
      {
228 229 230
        // 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 已提交
231
        // See more: https://github.com/zeit/next.js/issues/2617
232
        match: route('/static/:path*'),
233
        fn: async (req, res, params, parsedUrl) => {
T
Tim Neutkens 已提交
234
          const p = join(this.dir, 'static', ...(params.path || []))
235
          await this.serveStatic(req, res, p, parsedUrl)
236 237
        },
      },
L
Lukáš Huvar 已提交
238 239 240 241
      {
        match: route('/api/:path*'),
        fn: async (req, res, params, parsedUrl) => {
          const { pathname } = parsedUrl
L
Lukáš Huvar 已提交
242 243 244 245 246
          await this.handleApiRequest(
            req as NextApiRequest,
            res as NextApiResponse,
            pathname!
          )
L
Lukáš Huvar 已提交
247 248
        },
      },
T
Tim Neutkens 已提交
249
    ]
250

251 252 253 254
    if (fs.existsSync(this.publicDir)) {
      routes.push(...this.generatePublicRoutes())
    }

255
    if (this.nextConfig.useFileSystemPublicRoutes) {
J
Joe Haddad 已提交
256 257 258 259
      this.dynamicRoutes = this.nextConfig.experimental.dynamicRouting
        ? this.getDynamicRoutes()
        : []

260 261 262
      // 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.
263
      // See more: https://github.com/zeit/next.js/issues/2617
T
Tim Neutkens 已提交
264
      routes.push({
265
        match: route('/:path*'),
266
        fn: async (req, res, _params, parsedUrl) => {
T
Tim Neutkens 已提交
267
          const { pathname, query } = parsedUrl
268
          if (!pathname) {
269 270
            throw new Error('pathname is undefined')
          }
271

272
          await this.render(req, res, pathname, query, parsedUrl)
273
        },
T
Tim Neutkens 已提交
274
      })
275
    }
N
nkzawa 已提交
276

T
Tim Neutkens 已提交
277 278 279
    return routes
  }

L
Lukáš Huvar 已提交
280 281 282 283 284 285
  /**
   * Resolves `API` request, in development builds on demand
   * @param req http request
   * @param res http response
   * @param pathname path of request
   */
J
Joe Haddad 已提交
286
  private async handleApiRequest(
L
Lukáš Huvar 已提交
287 288
    req: NextApiRequest,
    res: NextApiResponse,
289
    pathname: string
J
Joe Haddad 已提交
290
  ) {
L
Lukáš Huvar 已提交
291 292 293 294 295 296 297
    const resolverFunction = await this.resolveApiRequest(pathname)
    if (resolverFunction === null) {
      res.statusCode = 404
      res.end('Not Found')
      return
    }

L
Lukáš Huvar 已提交
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
    try {
      // Parsing of cookies
      req.cookies = parseCookies(req.headers.cookie || '')
      // Parsing query string
      req.query = parseQuery(req)
      // // Parsing of body
      req.body = await parseBody(req)

      res.status = statusCode => sendStatusCode(res, statusCode)
      res.send = data => sendData(res, data)
      res.json = data => sendJson(res, data)

      const resolver = interopDefault(require(resolverFunction))
      resolver(req, res)
    } catch (e) {
      if (e instanceof ApiError) {
        sendError(res, e.statusCode, e.message)
      } else {
        sendError(res, 500, e.message)
      }
    }
L
Lukáš Huvar 已提交
319 320 321 322 323 324 325
  }

  /**
   * Resolves path to resolver function
   * @param pathname path of request
   */
  private resolveApiRequest(pathname: string) {
J
Joe Haddad 已提交
326 327 328
    return getPagePath(
      pathname,
      this.distDir,
329
      this.nextConfig.target === 'serverless'
J
Joe Haddad 已提交
330
    )
L
Lukáš Huvar 已提交
331 332
  }

333 334 335
  private generatePublicRoutes(): Route[] {
    const routes: Route[] = []
    const publicFiles = recursiveReadDirSync(this.publicDir)
336 337 338 339 340 341
    const serverBuildPath = join(
      this.distDir,
      this.nextConfig.target === 'serverless'
        ? SERVERLESS_DIRECTORY
        : SERVER_DIRECTORY
    )
342 343
    const pagesManifest = require(join(serverBuildPath, PAGES_MANIFEST))

344
    publicFiles.forEach(path => {
345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360
      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 已提交
361 362
  private getDynamicRoutes() {
    const manifest = require(this.buildManifest)
363 364
    const dynamicRoutedPages = Object.keys(manifest.pages).filter(
      isDynamicRoute
J
Joe Haddad 已提交
365
    )
366 367 368 369
    return getSortedRoutes(dynamicRoutedPages).map(page => ({
      page,
      match: getRouteMatcher(getRouteRegex(page)),
    }))
J
Joe Haddad 已提交
370 371
  }

J
Joe Haddad 已提交
372 373 374
  private async run(
    req: IncomingMessage,
    res: ServerResponse,
375
    parsedUrl: UrlWithParsedQuery
J
Joe Haddad 已提交
376
  ) {
377 378 379 380 381 382 383 384 385 386 387 388
    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
389 390 391
    }

    if (req.method === 'GET' || req.method === 'HEAD') {
392
      await this.render404(req, res, parsedUrl)
393 394
    } else {
      res.statusCode = 501
395
      res.end('Not Implemented')
N
nkzawa 已提交
396 397 398
    }
  }

J
Joe Haddad 已提交
399 400 401
  private async sendHTML(
    req: IncomingMessage,
    res: ServerResponse,
402
    html: string
J
Joe Haddad 已提交
403
  ) {
T
Tim Neutkens 已提交
404 405
    const { generateEtags, poweredByHeader } = this.renderOpts
    return sendHTML(req, res, html, { generateEtags, poweredByHeader })
406 407
  }

J
Joe Haddad 已提交
408 409 410 411 412
  public async render(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
413
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
414
  ): Promise<void> {
415 416
    const url: any = req.url
    if (isInternalUrl(url)) {
417 418 419
      return this.handleRequest(req, res, parsedUrl)
    }

420
    if (isBlockedPage(pathname)) {
421
      return this.render404(req, res, parsedUrl)
422 423
    }

424
    const html = await this.renderToHTML(req, res, pathname, query, {
J
Joe Haddad 已提交
425 426 427 428 429
      dataOnly:
        (this.renderOpts.ampBindInitData && Boolean(query.dataOnly)) ||
        (req.headers &&
          (req.headers.accept || '').indexOf('application/amp.bind+json') !==
            -1),
430
    })
431 432
    // Request was ended by the user
    if (html === null) {
433 434 435
      return
    }

436
    return this.sendHTML(req, res, html)
N
Naoyuki Kanezawa 已提交
437
  }
N
nkzawa 已提交
438

J
Joe Haddad 已提交
439
  private async findPageComponents(
J
Joe Haddad 已提交
440
    pathname: string,
441
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
442
  ) {
J
Joe Haddad 已提交
443 444
    const serverless =
      !this.renderOpts.dev && this.nextConfig.target === 'serverless'
J
JJ Kasper 已提交
445 446 447
    // try serving a static AMP version first
    if (query.amp) {
      try {
J
Joe Haddad 已提交
448 449 450 451
        return await loadComponents(
          this.distDir,
          this.buildId,
          (pathname === '/' ? '/index' : pathname) + '.amp',
452
          serverless
J
Joe Haddad 已提交
453
        )
J
JJ Kasper 已提交
454 455 456 457
      } catch (err) {
        if (err.code !== 'ENOENT') throw err
      }
    }
J
Joe Haddad 已提交
458 459 460 461
    return await loadComponents(
      this.distDir,
      this.buildId,
      pathname,
462
      serverless
J
Joe Haddad 已提交
463 464 465 466 467 468 469 470 471
    )
  }

  private async renderToHTMLWithComponents(
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
    result: LoadComponentsReturnType,
472
    opts: any
J
Joe Haddad 已提交
473
  ) {
J
JJ Kasper 已提交
474
    // handle static page
J
Joe Haddad 已提交
475 476 477 478
    if (typeof result.Component === 'string') {
      return result.Component
    }

J
JJ Kasper 已提交
479
    // handle serverless
J
Joe Haddad 已提交
480 481
    if (
      typeof result.Component === 'object' &&
J
JJ Kasper 已提交
482 483 484 485
      typeof result.Component.renderReqToHTML === 'function'
    ) {
      return result.Component.renderReqToHTML(req, res)
    }
J
Joe Haddad 已提交
486

487 488 489 490 491
    return renderToHTML(req, res, pathname, query, {
      ...result,
      ...opts,
      PageConfig: result.PageConfig,
    })
492 493
  }

J
Joe Haddad 已提交
494
  public renderToHTML(
J
Joe Haddad 已提交
495 496 497 498
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
    query: ParsedUrlQuery = {},
J
Joe Haddad 已提交
499 500 501 502 503 504 505
    {
      amphtml,
      dataOnly,
      hasAmp,
    }: {
      amphtml?: boolean
      hasAmp?: boolean
506 507
      dataOnly?: boolean
    } = {}
J
Joe Haddad 已提交
508
  ): Promise<string | null> {
J
Joe Haddad 已提交
509 510
    return this.findPageComponents(pathname, query)
      .then(
511
        result => {
J
Joe Haddad 已提交
512 513 514 515 516 517
          return this.renderToHTMLWithComponents(
            req,
            res,
            pathname,
            query,
            result,
518
            { ...this.renderOpts, amphtml, hasAmp, dataOnly }
J
Joe Haddad 已提交
519 520
          )
        },
521
        err => {
J
Joe Haddad 已提交
522 523 524 525 526 527 528 529 530 531 532
          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(
533
              result =>
J
Joe Haddad 已提交
534 535 536 537 538 539
                this.renderToHTMLWithComponents(
                  req,
                  res,
                  dynamicRoute.page,
                  { ...query, ...params },
                  result,
540 541
                  { ...this.renderOpts, amphtml, hasAmp, dataOnly }
                )
J
Joe Haddad 已提交
542 543 544 545
            )
          }

          return Promise.reject(err)
546
        }
J
Joe Haddad 已提交
547
      )
548
      .catch(err => {
J
Joe Haddad 已提交
549 550 551 552 553 554 555 556 557
        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 已提交
558 559
  }

J
Joe Haddad 已提交
560 561 562 563 564
  public async renderError(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    pathname: string,
565
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
566 567 568
  ): Promise<void> {
    res.setHeader(
      'Cache-Control',
569
      'no-cache, no-store, max-age=0, must-revalidate'
J
Joe Haddad 已提交
570
    )
N
Naoyuki Kanezawa 已提交
571
    const html = await this.renderErrorToHTML(err, req, res, pathname, query)
572
    if (html === null) {
573 574
      return
    }
575
    return this.sendHTML(req, res, html)
N
nkzawa 已提交
576 577
  }

J
Joe Haddad 已提交
578 579 580 581 582
  public async renderErrorToHTML(
    err: Error | null,
    req: IncomingMessage,
    res: ServerResponse,
    _pathname: string,
583
    query: ParsedUrlQuery = {}
J
Joe Haddad 已提交
584
  ) {
J
Joe Haddad 已提交
585
    const result = await this.findPageComponents('/_error', query)
586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604
    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 已提交
605 606
  }

J
Joe Haddad 已提交
607 608 609
  public async render404(
    req: IncomingMessage,
    res: ServerResponse,
610
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
611
  ): Promise<void> {
612 613
    const url: any = req.url
    const { pathname, query } = parsedUrl ? parsedUrl : parseUrl(url, true)
614
    if (!pathname) {
615 616
      throw new Error('pathname is undefined')
    }
N
Naoyuki Kanezawa 已提交
617
    res.statusCode = 404
618
    return this.renderError(null, req, res, pathname, query)
N
Naoyuki Kanezawa 已提交
619
  }
N
Naoyuki Kanezawa 已提交
620

J
Joe Haddad 已提交
621 622 623 624
  public async serveStatic(
    req: IncomingMessage,
    res: ServerResponse,
    path: string,
625
    parsedUrl?: UrlWithParsedQuery
J
Joe Haddad 已提交
626
  ): Promise<void> {
A
Arunoda Susiripala 已提交
627
    if (!this.isServeableUrl(path)) {
628
      return this.render404(req, res, parsedUrl)
A
Arunoda Susiripala 已提交
629 630
    }

N
Naoyuki Kanezawa 已提交
631
    try {
632
      await serveStatic(req, res, path)
N
Naoyuki Kanezawa 已提交
633
    } catch (err) {
T
Tim Neutkens 已提交
634
      if (err.code === 'ENOENT' || err.statusCode === 404) {
635
        this.render404(req, res, parsedUrl)
N
Naoyuki Kanezawa 已提交
636 637 638 639 640 641
      } else {
        throw err
      }
    }
  }

642
  private isServeableUrl(path: string): boolean {
A
Arunoda Susiripala 已提交
643 644
    const resolved = resolve(path)
    if (
645
      resolved.indexOf(join(this.distDir) + sep) !== 0 &&
646 647
      resolved.indexOf(join(this.dir, 'static') + sep) !== 0 &&
      resolved.indexOf(join(this.dir, 'public') + sep) !== 0
A
Arunoda Susiripala 已提交
648 649 650 651 652 653 654 655
    ) {
      // Seems like the user is trying to traverse the filesystem.
      return false
    }

    return true
  }

656
  private readBuildId(): string {
657 658 659 660 661
    const buildIdFile = join(this.distDir, BUILD_ID_FILE)
    try {
      return fs.readFileSync(buildIdFile, 'utf8').trim()
    } catch (err) {
      if (!fs.existsSync(buildIdFile)) {
J
Joe Haddad 已提交
662 663 664
        throw new Error(
          `Could not find a valid build in the '${
            this.distDir
665
          }' directory! Try building your app with 'next build' before starting the server.`
J
Joe Haddad 已提交
666
        )
667 668 669
      }

      throw err
670
    }
671
  }
672
}