/*! * finalhandler * Copyright(c) 2014-2015 Douglas Christopher Wilson * MIT Licensed */ 'use strict' /** * Module dependencies. * @private */ var debug = require('debug')('finalhandler') var escapeHtml = require('escape-html') var onFinished = require('on-finished') var statuses = require('statuses') var unpipe = require('unpipe') /** * Module variables. * @private */ /* istanbul ignore next */ var defer = typeof setImmediate === 'function' ? setImmediate : function (fn) { process.nextTick(fn.bind.apply(fn, arguments)) } var isFinished = onFinished.isFinished /** * 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 = Object.create(null) var status // ignore 404 on in-flight response if (!err && res._header) { debug('cannot 404 after headers sent') return } // unhandled error if (err) { // respect status code from error status = getErrorStatusCode(err) || res.statusCode // default status code to 500 if outside valid range if (typeof status !== 'number' || status < 400 || status > 599) { status = 500 } // respect err.headers if (err.headers && (err.status === status || err.statusCode === status)) { var keys = Object.keys(err.headers) for (var i = 0; i < keys.length; i++) { var key = keys[i] headers[key] = err.headers[key] } } // production gets a basic error message var msg = env === 'production' ? statuses[status] : err.stack || err.toString() msg = escapeHtml(msg) .replace(/\n/g, '
') .replace(/\x20{2}/g, '  ') + '\n' } else { status = 404 msg = 'Cannot ' + escapeHtml(req.method) + ' ' + escapeHtml(req.originalUrl || req.url) + '\n' } debug('default %s', status) // schedule onerror callback if (err && onerror) { defer(onerror, err, req, res) } // cannot actually respond if (res._header) { debug('cannot %d after headers sent', status) req.socket.destroy() return } // send response send(req, res, status, headers, msg) } } /** * 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 } /** * Send response. * * @param {IncomingMessage} req * @param {OutgoingMessage} res * @param {number} status * @param {object} headers * @param {string} body * @private */ function send (req, res, status, headers, body) { function write () { // response status res.statusCode = status res.statusMessage = statuses[status] // response headers var keys = Object.keys(headers) for (var i = 0; i < keys.length; i++) { var key = keys[i] res.setHeader(key, headers[key]) } // security header for content sniffing 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() }