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, ' ') 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 }