twitst4tz

twitter statistics web application
Log | Files | Refs | README | LICENSE

index.js (6518B)


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