353 lines
6.7 KiB
JavaScript
353 lines
6.7 KiB
JavaScript
/*!
|
|
* finalhandler
|
|
* Copyright(c) 2014-2022 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);
|
|
if (req.socket) {
|
|
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;
|
|
|
|
if (req.httpVersionMajor < 2) {
|
|
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]);
|
|
}
|
|
}
|