190 lines
4.0 KiB
JavaScript
190 lines
4.0 KiB
JavaScript
/*!
|
|
* 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, '<br>')
|
|
.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()
|
|
}
|