You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
331 lines
6.4 KiB
331 lines
6.4 KiB
/*! |
|
* finalhandler |
|
* Copyright(c) 2014-2017 Douglas Christopher Wilson |
|
* MIT Licensed |
|
*/ |
|
|
|
'use strict' |
|
|
|
/** |
|
* 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[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[status] |
|
|
|
// 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]) |
|
} |
|
}
|
|
|