| /*! |
| * 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]) |
| } |
| } |