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