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
|
|
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')
|
21
|
|
22
|
/**
|
23
|
* Module variables.
|
24
|
* @private
|
25
|
*/
|
26
|
|
27
|
var DOUBLE_SPACE_REGEXP = /\x20{2}/g
|
28
|
var NEWLINE_REGEXP = /\n/g
|
29
|
|
30
|
/* istanbul ignore next */
|
31
|
var defer = typeof setImmediate === 'function'
|
32
|
? setImmediate
|
33
|
: function (fn) { process.nextTick(fn.bind.apply(fn, arguments)) }
|
34
|
var isFinished = onFinished.isFinished
|
35
|
|
36
|
/**
|
37
|
* Create a minimal HTML document.
|
38
|
*
|
39
|
* @param {string} message
|
40
|
* @private
|
41
|
*/
|
42
|
|
43
|
function createHtmlDocument (message) {
|
44
|
var body = escapeHtml(message)
|
45
|
.replace(NEWLINE_REGEXP, '<br>')
|
46
|
.replace(DOUBLE_SPACE_REGEXP, ' ')
|
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
|
|
65
|
module.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
|
|
77
|
function 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
|
|
145
|
function 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
|
|
171
|
function 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
|
|
195
|
function 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
|
|
220
|
function 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
|
|
236
|
function 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
|
|
255
|
function 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
|
|
272
|
function 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
|
|
321
|
function 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
|
}
|