index.js
  1  /*!
  2   * finalhandler
  3   * Copyright(c) 2014-2022 Douglas Christopher Wilson
  4   * MIT Licensed
  5   */
  6  
  7  'use strict'
  8  
  9  /**
 10   * Module dependencies.
 11   * @private
 12   */
 13  
 14  var debug = require('debug')('finalhandler')
 15  var encodeUrl = require('encodeurl')
 16  var escapeHtml = require('escape-html')
 17  var onFinished = require('on-finished')
 18  var parseUrl = require('parseurl')
 19  var statuses = require('statuses')
 20  
 21  /**
 22   * Module variables.
 23   * @private
 24   */
 25  
 26  var isFinished = onFinished.isFinished
 27  
 28  /**
 29   * Create a minimal HTML document.
 30   *
 31   * @param {string} message
 32   * @private
 33   */
 34  
 35  function createHtmlDocument (message) {
 36    var body = escapeHtml(message)
 37      .replaceAll('\n', '<br>')
 38      .replaceAll('  ', ' &nbsp;')
 39  
 40    return '<!DOCTYPE html>\n' +
 41      '<html lang="en">\n' +
 42      '<head>\n' +
 43      '<meta charset="utf-8">\n' +
 44      '<title>Error</title>\n' +
 45      '</head>\n' +
 46      '<body>\n' +
 47      '<pre>' + body + '</pre>\n' +
 48      '</body>\n' +
 49      '</html>\n'
 50  }
 51  
 52  /**
 53   * Module exports.
 54   * @public
 55   */
 56  
 57  module.exports = finalhandler
 58  
 59  /**
 60   * Create a function to handle the final response.
 61   *
 62   * @param {Request} req
 63   * @param {Response} res
 64   * @param {Object} [options]
 65   * @return {Function}
 66   * @public
 67   */
 68  
 69  function finalhandler (req, res, options) {
 70    var opts = options || {}
 71  
 72    // get environment
 73    var env = opts.env || process.env.NODE_ENV || 'development'
 74  
 75    // get error callback
 76    var onerror = opts.onerror
 77  
 78    return function (err) {
 79      var headers
 80      var msg
 81      var status
 82  
 83      // ignore 404 on in-flight response
 84      if (!err && res.headersSent) {
 85        debug('cannot 404 after headers sent')
 86        return
 87      }
 88  
 89      // unhandled error
 90      if (err) {
 91        // respect status code from error
 92        status = getErrorStatusCode(err)
 93  
 94        if (status === undefined) {
 95          // fallback to status code on response
 96          status = getResponseStatusCode(res)
 97        } else {
 98          // respect headers from error
 99          headers = getErrorHeaders(err)
100        }
101  
102        // get error message
103        msg = getErrorMessage(err, status, env)
104      } else {
105        // not found
106        status = 404
107        msg = 'Cannot ' + req.method + ' ' + encodeUrl(getResourceName(req))
108      }
109  
110      debug('default %s', status)
111  
112      // schedule onerror callback
113      if (err && onerror) {
114        setImmediate(onerror, err, req, res)
115      }
116  
117      // cannot actually respond
118      if (res.headersSent) {
119        debug('cannot %d after headers sent', status)
120        if (req.socket) {
121          req.socket.destroy()
122        }
123        return
124      }
125  
126      // send response
127      send(req, res, status, headers, msg)
128    }
129  }
130  
131  /**
132   * Get headers from Error object.
133   *
134   * @param {Error} err
135   * @return {object}
136   * @private
137   */
138  
139  function getErrorHeaders (err) {
140    if (!err.headers || typeof err.headers !== 'object') {
141      return undefined
142    }
143  
144    return { ...err.headers }
145  }
146  
147  /**
148   * Get message from Error object, fallback to status message.
149   *
150   * @param {Error} err
151   * @param {number} status
152   * @param {string} env
153   * @return {string}
154   * @private
155   */
156  
157  function getErrorMessage (err, status, env) {
158    var msg
159  
160    if (env !== 'production') {
161      // use err.stack, which typically includes err.message
162      msg = err.stack
163  
164      // fallback to err.toString() when possible
165      if (!msg && typeof err.toString === 'function') {
166        msg = err.toString()
167      }
168    }
169  
170    return msg || statuses.message[status]
171  }
172  
173  /**
174   * Get status code from Error object.
175   *
176   * @param {Error} err
177   * @return {number}
178   * @private
179   */
180  
181  function getErrorStatusCode (err) {
182    // check err.status
183    if (typeof err.status === 'number' && err.status >= 400 && err.status < 600) {
184      return err.status
185    }
186  
187    // check err.statusCode
188    if (typeof err.statusCode === 'number' && err.statusCode >= 400 && err.statusCode < 600) {
189      return err.statusCode
190    }
191  
192    return undefined
193  }
194  
195  /**
196   * Get resource name for the request.
197   *
198   * This is typically just the original pathname of the request
199   * but will fallback to "resource" is that cannot be determined.
200   *
201   * @param {IncomingMessage} req
202   * @return {string}
203   * @private
204   */
205  
206  function getResourceName (req) {
207    try {
208      return parseUrl.original(req).pathname
209    } catch (e) {
210      return 'resource'
211    }
212  }
213  
214  /**
215   * Get status code from response.
216   *
217   * @param {OutgoingMessage} res
218   * @return {number}
219   * @private
220   */
221  
222  function getResponseStatusCode (res) {
223    var status = res.statusCode
224  
225    // default status code to 500 if outside valid range
226    if (typeof status !== 'number' || status < 400 || status > 599) {
227      status = 500
228    }
229  
230    return status
231  }
232  
233  /**
234   * Send response.
235   *
236   * @param {IncomingMessage} req
237   * @param {OutgoingMessage} res
238   * @param {number} status
239   * @param {object} headers
240   * @param {string} message
241   * @private
242   */
243  
244  function send (req, res, status, headers, message) {
245    function write () {
246      // response body
247      var body = createHtmlDocument(message)
248  
249      // response status
250      res.statusCode = status
251  
252      if (req.httpVersionMajor < 2) {
253        res.statusMessage = statuses.message[status]
254      }
255  
256      // remove any content headers
257      res.removeHeader('Content-Encoding')
258      res.removeHeader('Content-Language')
259      res.removeHeader('Content-Range')
260  
261      // response headers
262      for (const [key, value] of Object.entries(headers ?? {})) {
263        res.setHeader(key, value)
264      }
265  
266      // security headers
267      res.setHeader('Content-Security-Policy', "default-src 'none'")
268      res.setHeader('X-Content-Type-Options', 'nosniff')
269  
270      // standard headers
271      res.setHeader('Content-Type', 'text/html; charset=utf-8')
272      res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'))
273  
274      if (req.method === 'HEAD') {
275        res.end()
276        return
277      }
278  
279      res.end(body, 'utf8')
280    }
281  
282    if (isFinished(req)) {
283      write()
284      return
285    }
286  
287    // unpipe everything from the request
288    req.unpipe()
289  
290    // flush the request
291    onFinished(req, write)
292    req.resume()
293  }