3 * Copyright(c) 2014-2017 Douglas Christopher Wilson
10 * Module dependencies.
14 var debug = require('debug')('finalhandler')
15 var encodeUrl = require('encodeurl')
16 var escapeHtml = require('escape-html')
17 var onFinished = require('on-finished')
18 var parseUrl = require('parseurl')
19 var statuses = require('statuses')
20 var unpipe = require('unpipe')
27 var DOUBLE_SPACE_REGEXP = /\x20{2}/g
28 var NEWLINE_REGEXP = /\n/g
30 /* istanbul ignore next */
31 var defer = typeof setImmediate === 'function'
33 : function (fn) { process.nextTick(fn.bind.apply(fn, arguments)) }
34 var isFinished = onFinished.isFinished
37 * Create a minimal HTML document.
39 * @param {string} message
43 function createHtmlDocument (message) {
44 var body = escapeHtml(message)
45 .replace(NEWLINE_REGEXP, '<br>')
46 .replace(DOUBLE_SPACE_REGEXP, ' ')
48 return '<!DOCTYPE html>\n' +
49 '<html lang="en">\n' +
51 '<meta charset="utf-8">\n' +
52 '<title>Error</title>\n' +
55 '<pre>' + body + '</pre>\n' +
65 module.exports = finalhandler
68 * Create a function to handle the final response.
70 * @param {Request} req
71 * @param {Response} res
72 * @param {Object} [options]
77 function finalhandler (req, res, options) {
78 var opts = options || {}
81 var env = opts.env || process.env.NODE_ENV || 'development'
84 var onerror = opts.onerror
86 return function (err) {
91 // ignore 404 on in-flight response
92 if (!err && headersSent(res)) {
93 debug('cannot 404 after headers sent')
99 // respect status code from error
100 status = getErrorStatusCode(err)
102 if (status === undefined) {
103 // fallback to status code on response
104 status = getResponseStatusCode(res)
106 // respect headers from error
107 headers = getErrorHeaders(err)
111 msg = getErrorMessage(err, status, env)
115 msg = 'Cannot ' + req.method + ' ' + encodeUrl(getResourceName(req))
118 debug('default %s', status)
120 // schedule onerror callback
121 if (err && onerror) {
122 defer(onerror, err, req, res)
125 // cannot actually respond
126 if (headersSent(res)) {
127 debug('cannot %d after headers sent', status)
133 send(req, res, status, headers, msg)
138 * Get headers from Error object.
145 function getErrorHeaders (err) {
146 if (!err.headers || typeof err.headers !== 'object') {
150 var headers = Object.create(null)
151 var keys = Object.keys(err.headers)
153 for (var i = 0; i < keys.length; i++) {
155 headers[key] = err.headers[key]
162 * Get message from Error object, fallback to status message.
165 * @param {number} status
166 * @param {string} env
171 function getErrorMessage (err, status, env) {
174 if (env !== 'production') {
175 // use err.stack, which typically includes err.message
178 // fallback to err.toString() when possible
179 if (!msg && typeof err.toString === 'function') {
184 return msg || statuses[status]
188 * Get status code from Error object.
195 function getErrorStatusCode (err) {
197 if (typeof err.status === 'number' && err.status >= 400 && err.status < 600) {
201 // check err.statusCode
202 if (typeof err.statusCode === 'number' && err.statusCode >= 400 && err.statusCode < 600) {
203 return err.statusCode
210 * Get resource name for the request.
212 * This is typically just the original pathname of the request
213 * but will fallback to "resource" is that cannot be determined.
215 * @param {IncomingMessage} req
220 function getResourceName (req) {
222 return parseUrl.original(req).pathname
229 * Get status code from response.
231 * @param {OutgoingMessage} res
236 function getResponseStatusCode (res) {
237 var status = res.statusCode
239 // default status code to 500 if outside valid range
240 if (typeof status !== 'number' || status < 400 || status > 599) {
248 * Determine if the response headers have been sent.
250 * @param {object} res
255 function headersSent (res) {
256 return typeof res.headersSent !== 'boolean'
257 ? Boolean(res._header)
264 * @param {IncomingMessage} req
265 * @param {OutgoingMessage} res
266 * @param {number} status
267 * @param {object} headers
268 * @param {string} message
272 function send (req, res, status, headers, message) {
275 var body = createHtmlDocument(message)
278 res.statusCode = status
279 res.statusMessage = statuses[status]
282 setHeaders(res, headers)
285 res.setHeader('Content-Security-Policy', "default-src 'self'")
286 res.setHeader('X-Content-Type-Options', 'nosniff')
289 res.setHeader('Content-Type', 'text/html; charset=utf-8')
290 res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'))
292 if (req.method === 'HEAD') {
297 res.end(body, 'utf8')
300 if (isFinished(req)) {
305 // unpipe everything from the request
309 onFinished(req, write)
314 * Set response headers from an object.
316 * @param {OutgoingMessage} res
317 * @param {object} headers
321 function setHeaders (res, headers) {
326 var keys = Object.keys(headers)
327 for (var i = 0; i < keys.length; i++) {
329 res.setHeader(key, headers[key])