Spaces:
Sleeping
Sleeping
/*! | |
* finalhandler | |
* Copyright(c) 2014-2022 Douglas Christopher Wilson | |
* MIT Licensed | |
*/ | |
/** | |
* Module dependencies. | |
* @private | |
*/ | |
var debug = require('debug')('finalhandler') | |
var encodeUrl = require('encodeurl') | |
var escapeHtml = require('escape-html') | |
var onFinished = require('on-finished') | |
var parseUrl = require('parseurl') | |
var statuses = require('statuses') | |
var unpipe = require('unpipe') | |
/** | |
* Module variables. | |
* @private | |
*/ | |
var DOUBLE_SPACE_REGEXP = /\x20{2}/g | |
var NEWLINE_REGEXP = /\n/g | |
/* istanbul ignore next */ | |
var defer = typeof setImmediate === 'function' | |
? setImmediate | |
: function (fn) { process.nextTick(fn.bind.apply(fn, arguments)) } | |
var isFinished = onFinished.isFinished | |
/** | |
* Create a minimal HTML document. | |
* | |
* @param {string} message | |
* @private | |
*/ | |
function createHtmlDocument (message) { | |
var body = escapeHtml(message) | |
.replace(NEWLINE_REGEXP, '<br>') | |
.replace(DOUBLE_SPACE_REGEXP, ' ') | |
return '<!DOCTYPE html>\n' + | |
'<html lang="en">\n' + | |
'<head>\n' + | |
'<meta charset="utf-8">\n' + | |
'<title>Error</title>\n' + | |
'</head>\n' + | |
'<body>\n' + | |
'<pre>' + body + '</pre>\n' + | |
'</body>\n' + | |
'</html>\n' | |
} | |
/** | |
* Module exports. | |
* @public | |
*/ | |
module.exports = finalhandler | |
/** | |
* Create a function to handle the final response. | |
* | |
* @param {Request} req | |
* @param {Response} res | |
* @param {Object} [options] | |
* @return {Function} | |
* @public | |
*/ | |
function finalhandler (req, res, options) { | |
var opts = options || {} | |
// get environment | |
var env = opts.env || process.env.NODE_ENV || 'development' | |
// get error callback | |
var onerror = opts.onerror | |
return function (err) { | |
var headers | |
var msg | |
var status | |
// ignore 404 on in-flight response | |
if (!err && headersSent(res)) { | |
debug('cannot 404 after headers sent') | |
return | |
} | |
// unhandled error | |
if (err) { | |
// respect status code from error | |
status = getErrorStatusCode(err) | |
if (status === undefined) { | |
// fallback to status code on response | |
status = getResponseStatusCode(res) | |
} else { | |
// respect headers from error | |
headers = getErrorHeaders(err) | |
} | |
// get error message | |
msg = getErrorMessage(err, status, env) | |
} else { | |
// not found | |
status = 404 | |
msg = 'Cannot ' + req.method + ' ' + encodeUrl(getResourceName(req)) | |
} | |
debug('default %s', status) | |
// schedule onerror callback | |
if (err && onerror) { | |
defer(onerror, err, req, res) | |
} | |
// cannot actually respond | |
if (headersSent(res)) { | |
debug('cannot %d after headers sent', status) | |
req.socket.destroy() | |
return | |
} | |
// send response | |
send(req, res, status, headers, msg) | |
} | |
} | |
/** | |
* Get headers from Error object. | |
* | |
* @param {Error} err | |
* @return {object} | |
* @private | |
*/ | |
function getErrorHeaders (err) { | |
if (!err.headers || typeof err.headers !== 'object') { | |
return undefined | |
} | |
var headers = Object.create(null) | |
var keys = Object.keys(err.headers) | |
for (var i = 0; i < keys.length; i++) { | |
var key = keys[i] | |
headers[key] = err.headers[key] | |
} | |
return headers | |
} | |
/** | |
* Get message from Error object, fallback to status message. | |
* | |
* @param {Error} err | |
* @param {number} status | |
* @param {string} env | |
* @return {string} | |
* @private | |
*/ | |
function getErrorMessage (err, status, env) { | |
var msg | |
if (env !== 'production') { | |
// use err.stack, which typically includes err.message | |
msg = err.stack | |
// fallback to err.toString() when possible | |
if (!msg && typeof err.toString === 'function') { | |
msg = err.toString() | |
} | |
} | |
return msg || statuses.message[status] | |
} | |
/** | |
* Get status code from Error object. | |
* | |
* @param {Error} err | |
* @return {number} | |
* @private | |
*/ | |
function getErrorStatusCode (err) { | |
// check err.status | |
if (typeof err.status === 'number' && err.status >= 400 && err.status < 600) { | |
return err.status | |
} | |
// check err.statusCode | |
if (typeof err.statusCode === 'number' && err.statusCode >= 400 && err.statusCode < 600) { | |
return err.statusCode | |
} | |
return undefined | |
} | |
/** | |
* Get resource name for the request. | |
* | |
* This is typically just the original pathname of the request | |
* but will fallback to "resource" is that cannot be determined. | |
* | |
* @param {IncomingMessage} req | |
* @return {string} | |
* @private | |
*/ | |
function getResourceName (req) { | |
try { | |
return parseUrl.original(req).pathname | |
} catch (e) { | |
return 'resource' | |
} | |
} | |
/** | |
* Get status code from response. | |
* | |
* @param {OutgoingMessage} res | |
* @return {number} | |
* @private | |
*/ | |
function getResponseStatusCode (res) { | |
var status = res.statusCode | |
// default status code to 500 if outside valid range | |
if (typeof status !== 'number' || status < 400 || status > 599) { | |
status = 500 | |
} | |
return status | |
} | |
/** | |
* Determine if the response headers have been sent. | |
* | |
* @param {object} res | |
* @returns {boolean} | |
* @private | |
*/ | |
function headersSent (res) { | |
return typeof res.headersSent !== 'boolean' | |
? Boolean(res._header) | |
: res.headersSent | |
} | |
/** | |
* Send response. | |
* | |
* @param {IncomingMessage} req | |
* @param {OutgoingMessage} res | |
* @param {number} status | |
* @param {object} headers | |
* @param {string} message | |
* @private | |
*/ | |
function send (req, res, status, headers, message) { | |
function write () { | |
// response body | |
var body = createHtmlDocument(message) | |
// response status | |
res.statusCode = status | |
res.statusMessage = statuses.message[status] | |
// remove any content headers | |
res.removeHeader('Content-Encoding') | |
res.removeHeader('Content-Language') | |
res.removeHeader('Content-Range') | |
// response headers | |
setHeaders(res, headers) | |
// security headers | |
res.setHeader('Content-Security-Policy', "default-src 'none'") | |
res.setHeader('X-Content-Type-Options', 'nosniff') | |
// standard headers | |
res.setHeader('Content-Type', 'text/html; charset=utf-8') | |
res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8')) | |
if (req.method === 'HEAD') { | |
res.end() | |
return | |
} | |
res.end(body, 'utf8') | |
} | |
if (isFinished(req)) { | |
write() | |
return | |
} | |
// unpipe everything from the request | |
unpipe(req) | |
// flush the request | |
onFinished(req, write) | |
req.resume() | |
} | |
/** | |
* Set response headers from an object. | |
* | |
* @param {OutgoingMessage} res | |
* @param {object} headers | |
* @private | |
*/ | |
function setHeaders (res, headers) { | |
if (!headers) { | |
return | |
} | |
var keys = Object.keys(headers) | |
for (var i = 0; i < keys.length; i++) { | |
var key = keys[i] | |
res.setHeader(key, headers[key]) | |
} | |
} | |