UNPKG

6.52 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 '</html>\n'
58}
59
60/**
61 * Module exports.
62 * @public
63 */
64
65module.exports = finalhandler
66
67/**
68 * Create a function to handle the final response.
69 *
70 * @param {Request} req
71 * @param {Response} res
72 * @param {Object} [options]
73 * @return {Function}
74 * @public
75 */
76
77function finalhandler (req, res, options) {
78 var opts = options || {}
79
80 // get environment
81 var env = opts.env || process.env.NODE_ENV || 'development'
82
83 // get error callback
84 var onerror = opts.onerror
85
86 return function (err) {
87 var headers
88 var msg
89 var status
90
91 // ignore 404 on in-flight response
92 if (!err && headersSent(res)) {
93 debug('cannot 404 after headers sent')
94 return
95 }
96
97 // unhandled error
98 if (err) {
99 // respect status code from error
100 status = getErrorStatusCode(err)
101
102 if (status === undefined) {
103 // fallback to status code on response
104 status = getResponseStatusCode(res)
105 } else {
106 // respect headers from error
107 headers = getErrorHeaders(err)
108 }
109
110 // get error message
111 msg = getErrorMessage(err, status, env)
112 } else {
113 // not found
114 status = 404
115 msg = 'Cannot ' + req.method + ' ' + encodeUrl(getResourceName(req))
116 }
117
118 debug('default %s', status)
119
120 // schedule onerror callback
121 if (err && onerror) {
122 defer(onerror, err, req, res)
123 }
124
125 // cannot actually respond
126 if (headersSent(res)) {
127 debug('cannot %d after headers sent', status)
128 req.socket.destroy()
129 return
130 }
131
132 // send response
133 send(req, res, status, headers, msg)
134 }
135}
136
137/**
138 * Get headers from Error object.
139 *
140 * @param {Error} err
141 * @return {object}
142 * @private
143 */
144
145function getErrorHeaders (err) {
146 if (!err.headers || typeof err.headers !== 'object') {
147 return undefined
148 }
149
150 var headers = Object.create(null)
151 var keys = Object.keys(err.headers)
152
153 for (var i = 0; i < keys.length; i++) {
154 var key = keys[i]
155 headers[key] = err.headers[key]
156 }
157
158 return headers
159}
160
161/**
162 * Get message from Error object, fallback to status message.
163 *
164 * @param {Error} err
165 * @param {number} status
166 * @param {string} env
167 * @return {string}
168 * @private
169 */
170
171function getErrorMessage (err, status, env) {
172 var msg
173
174 if (env !== 'production') {
175 // use err.stack, which typically includes err.message
176 msg = err.stack
177
178 // fallback to err.toString() when possible
179 if (!msg && typeof err.toString === 'function') {
180 msg = err.toString()
181 }
182 }
183
184 return msg || statuses[status]
185}
186
187/**
188 * Get status code from Error object.
189 *
190 * @param {Error} err
191 * @return {number}
192 * @private
193 */
194
195function getErrorStatusCode (err) {
196 // check err.status
197 if (typeof err.status === 'number' && err.status >= 400 && err.status < 600) {
198 return err.status
199 }
200
201 // check err.statusCode
202 if (typeof err.statusCode === 'number' && err.statusCode >= 400 && err.statusCode < 600) {
203 return err.statusCode
204 }
205
206 return undefined
207}
208
209/**
210 * Get resource name for the request.
211 *
212 * This is typically just the original pathname of the request
213 * but will fallback to "resource" is that cannot be determined.
214 *
215 * @param {IncomingMessage} req
216 * @return {string}
217 * @private
218 */
219
220function getResourceName (req) {
221 try {
222 return parseUrl.original(req).pathname
223 } catch (e) {
224 return 'resource'
225 }
226}
227
228/**
229 * Get status code from response.
230 *
231 * @param {OutgoingMessage} res
232 * @return {number}
233 * @private
234 */
235
236function getResponseStatusCode (res) {
237 var status = res.statusCode
238
239 // default status code to 500 if outside valid range
240 if (typeof status !== 'number' || status < 400 || status > 599) {
241 status = 500
242 }
243
244 return status
245}
246
247/**
248 * Determine if the response headers have been sent.
249 *
250 * @param {object} res
251 * @returns {boolean}
252 * @private
253 */
254
255function headersSent (res) {
256 return typeof res.headersSent !== 'boolean'
257 ? Boolean(res._header)
258 : res.headersSent
259}
260
261/**
262 * Send response.
263 *
264 * @param {IncomingMessage} req
265 * @param {OutgoingMessage} res
266 * @param {number} status
267 * @param {object} headers
268 * @param {string} message
269 * @private
270 */
271
272function send (req, res, status, headers, message) {
273 function write () {
274 // response body
275 var body = createHtmlDocument(message)
276
277 // response status
278 res.statusCode = status
279 res.statusMessage = statuses[status]
280
281 // response headers
282 setHeaders(res, headers)
283
284 // security headers
285 res.setHeader('Content-Security-Policy', "default-src 'none'")
286 res.setHeader('X-Content-Type-Options', 'nosniff')
287
288 // standard headers
289 res.setHeader('Content-Type', 'text/html; charset=utf-8')
290 res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'))
291
292 if (req.method === 'HEAD') {
293 res.end()
294 return
295 }
296
297 res.end(body, 'utf8')
298 }
299
300 if (isFinished(req)) {
301 write()
302 return
303 }
304
305 // unpipe everything from the request
306 unpipe(req)
307
308 // flush the request
309 onFinished(req, write)
310 req.resume()
311}
312
313/**
314 * Set response headers from an object.
315 *
316 * @param {OutgoingMessage} res
317 * @param {object} headers
318 * @private
319 */
320
321function setHeaders (res, headers) {
322 if (!headers) {
323 return
324 }
325
326 var keys = Object.keys(headers)
327 for (var i = 0; i < keys.length; i++) {
328 var key = keys[i]
329 res.setHeader(key, headers[key])
330 }
331}