UNPKG

5.9 kBJavaScriptView Raw
1/*!
2 * finalhandler
3 * Copyright(c) 2014-2017 Douglas Christopher Wilson
4 * MIT Licensed
5 */
6
7'use strict'
8
9/**
10 * Module dependencies.
11 * @private
12 */
13
14var debug = require('debug')('finalhandler')
15var encodeUrl = require('encodeurl')
16var escapeHtml = require('escape-html')
17var onFinished = require('on-finished')
18var parseUrl = require('parseurl')
19var statuses = require('statuses')
20var unpipe = require('unpipe')
21
22/**
23 * Module variables.
24 * @private
25 */
26
27var DOUBLE_SPACE_REGEXP = /\x20{2}/g
28var NEWLINE_REGEXP = /\n/g
29
30/* istanbul ignore next */
31var defer = typeof setImmediate === 'function'
32 ? setImmediate
33 : function (fn) { process.nextTick(fn.bind.apply(fn, arguments)) }
34var isFinished = onFinished.isFinished
35
36/**
37 * Create a minimal HTML document.
38 *
39 * @param {string} message
40 * @private
41 */
42
43function createHtmlDocument (message) {
44 var body = escapeHtml(message)
45 .replace(NEWLINE_REGEXP, '<br>')
46 .replace(DOUBLE_SPACE_REGEXP, ' &nbsp;')
47
48 return '<!DOCTYPE html>\n' +
49 '<html lang="en">\n' +
50 '<head>\n' +
51 '<meta charset="utf-8">\n' +
52 '<title>Error</title>\n' +
53 '</head>\n' +
54 '<body>\n' +
55 '<pre>' + body + '</pre>\n' +
56 '</body>\n'
57}
58
59/**
60 * Module exports.
61 * @public
62 */
63
64module.exports = finalhandler
65
66/**
67 * Create a function to handle the final response.
68 *
69 * @param {Request} req
70 * @param {Response} res
71 * @param {Object} [options]
72 * @return {Function}
73 * @public
74 */
75
76function finalhandler (req, res, options) {
77 var opts = options || {}
78
79 // get environment
80 var env = opts.env || process.env.NODE_ENV || 'development'
81
82 // get error callback
83 var onerror = opts.onerror
84
85 return function (err) {
86 var headers
87 var msg
88 var status
89
90 // ignore 404 on in-flight response
91 if (!err && res._header) {
92 debug('cannot 404 after headers sent')
93 return
94 }
95
96 // unhandled error
97 if (err) {
98 // respect status code from error
99 status = getErrorStatusCode(err)
100
101 // respect headers from error
102 if (status !== undefined) {
103 headers = getErrorHeaders(err)
104 }
105
106 // fallback to status code on response
107 if (status === undefined) {
108 status = getResponseStatusCode(res)
109 }
110
111 // get error message
112 msg = getErrorMessage(err, status, env)
113 } else {
114 // not found
115 status = 404
116 msg = 'Cannot ' + req.method + ' ' + encodeUrl(parseUrl.original(req).pathname)
117 }
118
119 debug('default %s', status)
120
121 // schedule onerror callback
122 if (err && onerror) {
123 defer(onerror, err, req, res)
124 }
125
126 // cannot actually respond
127 if (res._header) {
128 debug('cannot %d after headers sent', status)
129 req.socket.destroy()
130 return
131 }
132
133 // send response
134 send(req, res, status, headers, msg)
135 }
136}
137
138/**
139 * Get headers from Error object.
140 *
141 * @param {Error} err
142 * @return {object}
143 * @private
144 */
145
146function getErrorHeaders (err) {
147 if (!err.headers || typeof err.headers !== 'object') {
148 return undefined
149 }
150
151 var headers = Object.create(null)
152 var keys = Object.keys(err.headers)
153
154 for (var i = 0; i < keys.length; i++) {
155 var key = keys[i]
156 headers[key] = err.headers[key]
157 }
158
159 return headers
160}
161
162/**
163 * Get message from Error object, fallback to status message.
164 *
165 * @param {Error} err
166 * @param {number} status
167 * @param {string} env
168 * @return {string}
169 * @private
170 */
171
172function getErrorMessage (err, status, env) {
173 var msg
174
175 if (env !== 'production') {
176 // use err.stack, which typically includes err.message
177 msg = err.stack
178
179 // fallback to err.toString() when possible
180 if (!msg && typeof err.toString === 'function') {
181 msg = err.toString()
182 }
183 }
184
185 return msg || statuses[status]
186}
187
188/**
189 * Get status code from Error object.
190 *
191 * @param {Error} err
192 * @return {number}
193 * @private
194 */
195
196function getErrorStatusCode (err) {
197 // check err.status
198 if (typeof err.status === 'number' && err.status >= 400 && err.status < 600) {
199 return err.status
200 }
201
202 // check err.statusCode
203 if (typeof err.statusCode === 'number' && err.statusCode >= 400 && err.statusCode < 600) {
204 return err.statusCode
205 }
206
207 return undefined
208}
209
210/**
211 * Get status code from response.
212 *
213 * @param {OutgoingMessage} res
214 * @return {number}
215 * @private
216 */
217
218function getResponseStatusCode (res) {
219 var status = res.statusCode
220
221 // default status code to 500 if outside valid range
222 if (typeof status !== 'number' || status < 400 || status > 599) {
223 status = 500
224 }
225
226 return status
227}
228
229/**
230 * Send response.
231 *
232 * @param {IncomingMessage} req
233 * @param {OutgoingMessage} res
234 * @param {number} status
235 * @param {object} headers
236 * @param {string} message
237 * @private
238 */
239
240function send (req, res, status, headers, message) {
241 function write () {
242 // response body
243 var body = createHtmlDocument(message)
244
245 // response status
246 res.statusCode = status
247 res.statusMessage = statuses[status]
248
249 // response headers
250 setHeaders(res, headers)
251
252 // security headers
253 res.setHeader('Content-Security-Policy', "default-src 'self'")
254 res.setHeader('X-Content-Type-Options', 'nosniff')
255
256 // standard headers
257 res.setHeader('Content-Type', 'text/html; charset=utf-8')
258 res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'))
259
260 if (req.method === 'HEAD') {
261 res.end()
262 return
263 }
264
265 res.end(body, 'utf8')
266 }
267
268 if (isFinished(req)) {
269 write()
270 return
271 }
272
273 // unpipe everything from the request
274 unpipe(req)
275
276 // flush the request
277 onFinished(req, write)
278 req.resume()
279}
280
281/**
282 * Set response headers from an object.
283 *
284 * @param {OutgoingMessage} res
285 * @param {object} headers
286 * @private
287 */
288
289function setHeaders (res, headers) {
290 if (!headers) {
291 return
292 }
293
294 var keys = Object.keys(headers)
295 for (var i = 0; i < keys.length; i++) {
296 var key = keys[i]
297 res.setHeader(key, headers[key])
298 }
299}