strong-error-handler 3.2.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.travis.yml +6 -0
- package/CHANGES.md +40 -0
- package/README.md +31 -26
- package/index.d.ts +1 -0
- package/lib/clone.js +7 -2
- package/lib/content-negotiation.js +13 -13
- package/lib/data-builder.js +4 -4
- package/lib/handler.js +14 -15
- package/lib/logger.js +5 -5
- package/lib/send-html.js +12 -12
- package/lib/send-json.js +10 -2
- package/lib/send-xml.js +8 -4
- package/package.json +13 -11
- package/test/handler.test.js +1016 -0
- package/views/default-error.ejs +1 -1
|
@@ -0,0 +1,1016 @@
|
|
|
1
|
+
// Copyright IBM Corp. 2016,2018. All Rights Reserved.
|
|
2
|
+
// Node module: strong-error-handler
|
|
3
|
+
// This file is licensed under the MIT License.
|
|
4
|
+
// License text available at https://opensource.org/licenses/MIT
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const cloneAllProperties = require('../lib/clone.js');
|
|
9
|
+
const debug = require('debug')('test');
|
|
10
|
+
const expect = require('chai').expect;
|
|
11
|
+
const express = require('express');
|
|
12
|
+
const strongErrorHandler = require('..');
|
|
13
|
+
const supertest = require('supertest');
|
|
14
|
+
const util = require('util');
|
|
15
|
+
|
|
16
|
+
describe('strong-error-handler', function() {
|
|
17
|
+
before(setupHttpServerAndClient);
|
|
18
|
+
beforeEach(resetRequestHandler);
|
|
19
|
+
after(stopHttpServerAndClient);
|
|
20
|
+
|
|
21
|
+
it('sets nosniff header', function(done) {
|
|
22
|
+
givenErrorHandlerForError();
|
|
23
|
+
request.get('/')
|
|
24
|
+
.expect('X-Content-Type-Options', 'nosniff')
|
|
25
|
+
.expect(500, done);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('handles response headers already sent', function(done) {
|
|
29
|
+
givenErrorHandlerForError();
|
|
30
|
+
const handler = _requestHandler;
|
|
31
|
+
_requestHandler = function(req, res, next) {
|
|
32
|
+
res.end('empty');
|
|
33
|
+
process.nextTick(function() {
|
|
34
|
+
handler(req, res, next);
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
request.get('/').expect(200, 'empty', done);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
context('status code', function() {
|
|
42
|
+
it('converts non-error "err.status" to 500', function(done) {
|
|
43
|
+
givenErrorHandlerForError(new ErrorWithProps({status: 200}));
|
|
44
|
+
request.get('/').expect(500, done);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('converts non-error "err.statusCode" to 500', function(done) {
|
|
48
|
+
givenErrorHandlerForError(new ErrorWithProps({statusCode: 200}));
|
|
49
|
+
request.get('/').expect(500, done);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('uses the value from "err.status"', function(done) {
|
|
53
|
+
givenErrorHandlerForError(new ErrorWithProps({status: 404}));
|
|
54
|
+
request.get('/').expect(404, done);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('uses the value from "err.statusCode"', function(done) {
|
|
58
|
+
givenErrorHandlerForError(new ErrorWithProps({statusCode: 404}));
|
|
59
|
+
request.get('/').expect(404, done);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('prefers "err.statusCode" over "err.status"', function(done) {
|
|
63
|
+
givenErrorHandlerForError(new ErrorWithProps({
|
|
64
|
+
statusCode: 400,
|
|
65
|
+
status: 404,
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
request.get('/').expect(400, done);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('handles error from `res.statusCode`', function(done) {
|
|
72
|
+
givenErrorHandlerForError();
|
|
73
|
+
const handler = _requestHandler;
|
|
74
|
+
_requestHandler = function(req, res, next) {
|
|
75
|
+
res.statusCode = 507;
|
|
76
|
+
handler(req, res, next);
|
|
77
|
+
};
|
|
78
|
+
request.get('/').expect(
|
|
79
|
+
507,
|
|
80
|
+
{error: {statusCode: 507, message: 'Insufficient Storage'}},
|
|
81
|
+
done,
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
context('logging', function() {
|
|
87
|
+
let logs;
|
|
88
|
+
|
|
89
|
+
beforeEach(redirectConsoleError);
|
|
90
|
+
afterEach(restoreConsoleError);
|
|
91
|
+
|
|
92
|
+
it('logs by default', function(done) {
|
|
93
|
+
givenErrorHandlerForError(new Error(), {
|
|
94
|
+
// explicitly set to undefined to prevent givenErrorHandlerForError
|
|
95
|
+
// from disabling this option
|
|
96
|
+
log: undefined,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
request.get('/').end(function(err) {
|
|
100
|
+
if (err) return done(err);
|
|
101
|
+
expect(logs).to.have.length(1);
|
|
102
|
+
done();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('honours options.log=false', function(done) {
|
|
107
|
+
givenErrorHandlerForError(new Error(), {log: false});
|
|
108
|
+
|
|
109
|
+
request.get('/api').end(function(err) {
|
|
110
|
+
if (err) return done(err);
|
|
111
|
+
expect(logs).to.have.length(0);
|
|
112
|
+
done();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('honours options.log=true', function(done) {
|
|
117
|
+
givenErrorHandlerForError(new Error(), {log: true});
|
|
118
|
+
|
|
119
|
+
request.get('/api').end(function(err) {
|
|
120
|
+
if (err) return done(err);
|
|
121
|
+
expect(logs).to.have.length(1);
|
|
122
|
+
done();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('includes relevant information in the log message', function(done) {
|
|
127
|
+
givenErrorHandlerForError(new TypeError('ERROR-NAME'), {log: true});
|
|
128
|
+
|
|
129
|
+
request.get('/api').end(function(err) {
|
|
130
|
+
if (err) return done(err);
|
|
131
|
+
|
|
132
|
+
const msg = logs[0];
|
|
133
|
+
// the request method
|
|
134
|
+
expect(msg).to.contain('GET');
|
|
135
|
+
// the request path
|
|
136
|
+
expect(msg).to.contain('/api');
|
|
137
|
+
// the error name & message
|
|
138
|
+
expect(msg).to.contain('TypeError: ERROR-NAME');
|
|
139
|
+
// the stack
|
|
140
|
+
expect(msg).to.contain(__filename);
|
|
141
|
+
|
|
142
|
+
done();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('handles array argument', function(done) {
|
|
147
|
+
givenErrorHandlerForError(
|
|
148
|
+
[new TypeError('ERR1'), new Error('ERR2')],
|
|
149
|
+
{log: true},
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
request.get('/api').end(function(err) {
|
|
153
|
+
if (err) return done(err);
|
|
154
|
+
|
|
155
|
+
const msg = logs[0];
|
|
156
|
+
// the request method
|
|
157
|
+
expect(msg).to.contain('GET');
|
|
158
|
+
// the request path
|
|
159
|
+
expect(msg).to.contain('/api');
|
|
160
|
+
// the error name & message for all errors
|
|
161
|
+
expect(msg).to.contain('TypeError: ERR1');
|
|
162
|
+
expect(msg).to.contain('Error: ERR2');
|
|
163
|
+
// verify that stacks are included too
|
|
164
|
+
expect(msg).to.contain(__filename);
|
|
165
|
+
|
|
166
|
+
done();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('handles non-Error argument', function(done) {
|
|
171
|
+
givenErrorHandlerForError('STRING ERROR', {log: true});
|
|
172
|
+
request.get('/').end(function(err) {
|
|
173
|
+
if (err) return done(err);
|
|
174
|
+
const msg = logs[0];
|
|
175
|
+
expect(msg).to.contain('STRING ERROR');
|
|
176
|
+
done();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const _consoleError = console.error;
|
|
181
|
+
function redirectConsoleError() {
|
|
182
|
+
logs = [];
|
|
183
|
+
console.error = function() {
|
|
184
|
+
const msg = util.format.apply(util, arguments);
|
|
185
|
+
logs.push(msg);
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function restoreConsoleError() {
|
|
190
|
+
console.error = _consoleError;
|
|
191
|
+
logs = [];
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
context('JSON response', function() {
|
|
196
|
+
it('contains all error properties when debug=true', function(done) {
|
|
197
|
+
const error = new ErrorWithProps({
|
|
198
|
+
message: 'a test error message',
|
|
199
|
+
code: 'MACHINE_READABLE_CODE',
|
|
200
|
+
details: 'some details',
|
|
201
|
+
extra: 'sensitive data',
|
|
202
|
+
});
|
|
203
|
+
givenErrorHandlerForError(error, {debug: true});
|
|
204
|
+
|
|
205
|
+
requestJson().end(function(err, res) {
|
|
206
|
+
if (err) return done(err);
|
|
207
|
+
|
|
208
|
+
const expectedData = {
|
|
209
|
+
statusCode: 500,
|
|
210
|
+
message: 'a test error message',
|
|
211
|
+
name: 'ErrorWithProps',
|
|
212
|
+
code: 'MACHINE_READABLE_CODE',
|
|
213
|
+
details: 'some details',
|
|
214
|
+
extra: 'sensitive data',
|
|
215
|
+
stack: error.stack,
|
|
216
|
+
};
|
|
217
|
+
expect(res.body).to.have.property('error');
|
|
218
|
+
expect(res.body.error).to.eql(expectedData);
|
|
219
|
+
done();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('includes code property for 4xx status codes when debug=false',
|
|
224
|
+
function(done) {
|
|
225
|
+
const error = new ErrorWithProps({
|
|
226
|
+
statusCode: 400,
|
|
227
|
+
message: 'error with code',
|
|
228
|
+
name: 'ErrorWithCode',
|
|
229
|
+
code: 'MACHINE_READABLE_CODE',
|
|
230
|
+
});
|
|
231
|
+
givenErrorHandlerForError(error, {debug: false});
|
|
232
|
+
|
|
233
|
+
requestJson().end(function(err, res) {
|
|
234
|
+
if (err) return done(err);
|
|
235
|
+
|
|
236
|
+
const expectedData = {
|
|
237
|
+
statusCode: 400,
|
|
238
|
+
message: 'error with code',
|
|
239
|
+
name: 'ErrorWithCode',
|
|
240
|
+
code: 'MACHINE_READABLE_CODE',
|
|
241
|
+
};
|
|
242
|
+
expect(res.body).to.have.property('error');
|
|
243
|
+
expect(res.body.error).to.eql(expectedData);
|
|
244
|
+
done();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('excludes code property for 5xx status codes when debug=false',
|
|
249
|
+
function(done) {
|
|
250
|
+
const error = new ErrorWithProps({
|
|
251
|
+
statusCode: 500,
|
|
252
|
+
code: 'MACHINE_READABLE_CODE',
|
|
253
|
+
});
|
|
254
|
+
givenErrorHandlerForError(error, {debug: false});
|
|
255
|
+
|
|
256
|
+
requestJson().end(function(err, res) {
|
|
257
|
+
if (err) return done(err);
|
|
258
|
+
|
|
259
|
+
const expectedData = {
|
|
260
|
+
statusCode: 500,
|
|
261
|
+
message: 'Internal Server Error',
|
|
262
|
+
};
|
|
263
|
+
expect(res.body).to.have.property('error');
|
|
264
|
+
expect(res.body.error).to.eql(expectedData);
|
|
265
|
+
done();
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('contains non-enumerable Error properties when debug=true',
|
|
270
|
+
function(done) {
|
|
271
|
+
const error = new Error('a test error message');
|
|
272
|
+
givenErrorHandlerForError(error, {debug: true});
|
|
273
|
+
requestJson().end(function(err, res) {
|
|
274
|
+
if (err) return done(err);
|
|
275
|
+
expect(res.body).to.have.property('error');
|
|
276
|
+
const resError = res.body.error;
|
|
277
|
+
expect(resError).to.have.property('name', 'Error');
|
|
278
|
+
expect(resError).to.have.property('message',
|
|
279
|
+
'a test error message');
|
|
280
|
+
expect(resError).to.have.property('stack', error.stack);
|
|
281
|
+
done();
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('should allow setting safe fields when status=5xx', function(done) {
|
|
286
|
+
const error = new ErrorWithProps({
|
|
287
|
+
name: 'Error',
|
|
288
|
+
safeField: 'SAFE',
|
|
289
|
+
unsafeField: 'UNSAFE',
|
|
290
|
+
});
|
|
291
|
+
givenErrorHandlerForError(error, {
|
|
292
|
+
safeFields: ['safeField'],
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
requestJson().end(function(err, res) {
|
|
296
|
+
if (err) return done(err);
|
|
297
|
+
|
|
298
|
+
expect(res.body).to.have.property('error');
|
|
299
|
+
expect(res.body.error).to.have.property('safeField', 'SAFE');
|
|
300
|
+
expect(res.body.error).not.to.have.property('unsafeField');
|
|
301
|
+
|
|
302
|
+
done();
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('safe fields falls back to existing data', function(done) {
|
|
307
|
+
const error = new ErrorWithProps({
|
|
308
|
+
name: 'Error',
|
|
309
|
+
isSafe: false,
|
|
310
|
+
});
|
|
311
|
+
givenErrorHandlerForError(error, {
|
|
312
|
+
safeFields: ['statusCode', 'isSafe'],
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
requestJson().end(function(err, res) {
|
|
316
|
+
if (err) return done(err);
|
|
317
|
+
expect(res.body.error.statusCode).to.equal(500);
|
|
318
|
+
expect(res.body.error.isSafe).to.equal(false);
|
|
319
|
+
|
|
320
|
+
done();
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('should allow setting safe fields when status=4xx', function(done) {
|
|
325
|
+
const error = new ErrorWithProps({
|
|
326
|
+
name: 'Error',
|
|
327
|
+
statusCode: 422,
|
|
328
|
+
safeField: 'SAFE',
|
|
329
|
+
unsafeField: 'UNSAFE',
|
|
330
|
+
});
|
|
331
|
+
givenErrorHandlerForError(error, {
|
|
332
|
+
safeFields: ['safeField'],
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
requestJson().end(function(err, res) {
|
|
336
|
+
if (err) return done(err);
|
|
337
|
+
|
|
338
|
+
expect(res.body).to.have.property('error');
|
|
339
|
+
expect(res.body.error).to.have.property('safeField', 'SAFE');
|
|
340
|
+
expect(res.body.error).not.to.have.property('unsafeField');
|
|
341
|
+
|
|
342
|
+
done();
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('contains subset of properties when status=4xx', function(done) {
|
|
347
|
+
const error = new ErrorWithProps({
|
|
348
|
+
name: 'ValidationError',
|
|
349
|
+
message: 'The model instance is not valid.',
|
|
350
|
+
statusCode: 422,
|
|
351
|
+
details: 'some details',
|
|
352
|
+
extra: 'sensitive data',
|
|
353
|
+
});
|
|
354
|
+
givenErrorHandlerForError(error);
|
|
355
|
+
|
|
356
|
+
requestJson().end(function(err, res) {
|
|
357
|
+
if (err) return done(err);
|
|
358
|
+
|
|
359
|
+
expect(res.body).to.have.property('error');
|
|
360
|
+
expect(res.body.error).to.eql({
|
|
361
|
+
name: 'ValidationError',
|
|
362
|
+
message: 'The model instance is not valid.',
|
|
363
|
+
statusCode: 422,
|
|
364
|
+
details: 'some details',
|
|
365
|
+
// notice the property "extra" is not included
|
|
366
|
+
});
|
|
367
|
+
done();
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('contains only safe info when status=5xx', function(done) {
|
|
372
|
+
// Mock an error reported by fs.readFile
|
|
373
|
+
const error = new ErrorWithProps({
|
|
374
|
+
name: 'Error',
|
|
375
|
+
message: 'ENOENT: no such file or directory, open "/etc/passwd"',
|
|
376
|
+
errno: -2,
|
|
377
|
+
code: 'ENOENT',
|
|
378
|
+
syscall: 'open',
|
|
379
|
+
path: '/etc/password',
|
|
380
|
+
});
|
|
381
|
+
givenErrorHandlerForError(error);
|
|
382
|
+
|
|
383
|
+
requestJson().end(function(err, res) {
|
|
384
|
+
if (err) return done(err);
|
|
385
|
+
|
|
386
|
+
expect(res.body).to.have.property('error');
|
|
387
|
+
expect(res.body.error).to.eql({
|
|
388
|
+
statusCode: 500,
|
|
389
|
+
message: 'Internal Server Error',
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
done();
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('handles array argument as 500 when debug=false', function(done) {
|
|
397
|
+
const errors = [new Error('ERR1'), new Error('ERR2'), 'ERR STRING'];
|
|
398
|
+
givenErrorHandlerForError(errors);
|
|
399
|
+
|
|
400
|
+
requestJson().expect(500).end(function(err, res) {
|
|
401
|
+
if (err) return done(err);
|
|
402
|
+
const data = res.body.error;
|
|
403
|
+
expect(data).to.have.property('message').that.match(/multiple errors/);
|
|
404
|
+
expect(data).to.have.property('details').eql([
|
|
405
|
+
{statusCode: 500, message: 'Internal Server Error'},
|
|
406
|
+
{statusCode: 500, message: 'Internal Server Error'},
|
|
407
|
+
{statusCode: 500, message: 'Internal Server Error'},
|
|
408
|
+
]);
|
|
409
|
+
done();
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('returns all array items when debug=true', function(done) {
|
|
414
|
+
const testError = new ErrorWithProps({
|
|
415
|
+
message: 'expected test error',
|
|
416
|
+
statusCode: 400,
|
|
417
|
+
});
|
|
418
|
+
const anotherError = new ErrorWithProps({
|
|
419
|
+
message: 'another expected error',
|
|
420
|
+
statusCode: 500,
|
|
421
|
+
});
|
|
422
|
+
const errors = [testError, anotherError, 'ERR STRING'];
|
|
423
|
+
givenErrorHandlerForError(errors, {debug: true});
|
|
424
|
+
|
|
425
|
+
requestJson().expect(500).end(function(err, res) {
|
|
426
|
+
if (err) return done(err);
|
|
427
|
+
|
|
428
|
+
const data = res.body.error;
|
|
429
|
+
expect(data).to.have.property('message').that.match(/multiple errors/);
|
|
430
|
+
|
|
431
|
+
const expectedDetails = [
|
|
432
|
+
getExpectedErrorData(testError),
|
|
433
|
+
getExpectedErrorData(anotherError),
|
|
434
|
+
{message: 'ERR STRING', statusCode: 500},
|
|
435
|
+
];
|
|
436
|
+
expect(data).to.have.property('details').to.eql(expectedDetails);
|
|
437
|
+
done();
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it('includes safeFields of array items when debug=false', (done) => {
|
|
442
|
+
const internalError = new ErrorWithProps({
|
|
443
|
+
message: 'a test error message',
|
|
444
|
+
code: 'MACHINE_READABLE_CODE',
|
|
445
|
+
details: 'some details',
|
|
446
|
+
extra: 'sensitive data',
|
|
447
|
+
});
|
|
448
|
+
const validationError = new ErrorWithProps({
|
|
449
|
+
name: 'ValidationError',
|
|
450
|
+
message: 'The model instance is not valid.',
|
|
451
|
+
statusCode: 422,
|
|
452
|
+
code: 'VALIDATION_ERROR',
|
|
453
|
+
details: 'some details',
|
|
454
|
+
extra: 'sensitive data',
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
const errors = [internalError, validationError, 'ERR STRING'];
|
|
458
|
+
givenErrorHandlerForError(errors, {
|
|
459
|
+
debug: false,
|
|
460
|
+
safeFields: ['code'],
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
requestJson().end(function(err, res) {
|
|
464
|
+
if (err) return done(err);
|
|
465
|
+
const data = res.body.error;
|
|
466
|
+
|
|
467
|
+
const expectedInternalError = {
|
|
468
|
+
statusCode: 500,
|
|
469
|
+
message: 'Internal Server Error',
|
|
470
|
+
code: 'MACHINE_READABLE_CODE',
|
|
471
|
+
// notice the property "extra" is not included
|
|
472
|
+
};
|
|
473
|
+
const expectedValidationError = {
|
|
474
|
+
statusCode: 422,
|
|
475
|
+
message: 'The model instance is not valid.',
|
|
476
|
+
name: 'ValidationError',
|
|
477
|
+
code: 'VALIDATION_ERROR',
|
|
478
|
+
details: 'some details',
|
|
479
|
+
// notice the property "extra" is not included
|
|
480
|
+
};
|
|
481
|
+
const expectedErrorFromString = {
|
|
482
|
+
message: 'Internal Server Error',
|
|
483
|
+
statusCode: 500,
|
|
484
|
+
};
|
|
485
|
+
const expectedDetails = [
|
|
486
|
+
expectedInternalError,
|
|
487
|
+
expectedValidationError,
|
|
488
|
+
expectedErrorFromString,
|
|
489
|
+
];
|
|
490
|
+
|
|
491
|
+
expect(data).to.have.property('message').that.match(/multiple errors/);
|
|
492
|
+
expect(data).to.have.property('details').to.eql(expectedDetails);
|
|
493
|
+
done();
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('handles non-Error argument as 500 when debug=false', function(done) {
|
|
498
|
+
givenErrorHandlerForError('Error Message', {debug: false});
|
|
499
|
+
requestJson().expect(500).end(function(err, res) {
|
|
500
|
+
if (err) return done(err);
|
|
501
|
+
|
|
502
|
+
expect(res.body.error).to.eql({
|
|
503
|
+
statusCode: 500,
|
|
504
|
+
message: 'Internal Server Error',
|
|
505
|
+
});
|
|
506
|
+
done();
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('returns non-Error argument in message when debug=true', function(done) {
|
|
511
|
+
givenErrorHandlerForError('Error Message', {debug: true});
|
|
512
|
+
requestJson().expect(500).end(function(err, res) {
|
|
513
|
+
if (err) return done(err);
|
|
514
|
+
|
|
515
|
+
expect(res.body.error).to.eql({
|
|
516
|
+
statusCode: 500,
|
|
517
|
+
message: 'Error Message',
|
|
518
|
+
});
|
|
519
|
+
done();
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
it('handles Error objects containing circular properties', function(done) {
|
|
524
|
+
const circularObject = {};
|
|
525
|
+
circularObject.recursiveProp = circularObject;
|
|
526
|
+
const error = new ErrorWithProps({
|
|
527
|
+
statusCode: 422,
|
|
528
|
+
message: 'The model instance is not valid.',
|
|
529
|
+
name: 'ValidationError',
|
|
530
|
+
code: 'VALIDATION_ERROR',
|
|
531
|
+
details: circularObject,
|
|
532
|
+
});
|
|
533
|
+
givenErrorHandlerForError(error, {debug: true});
|
|
534
|
+
requestJson().end(function(err, res) {
|
|
535
|
+
if (err) return done(err);
|
|
536
|
+
expect(res.body).to.have.property('error');
|
|
537
|
+
expect(res.body.error).to.have.property('details');
|
|
538
|
+
expect(res.body.error.details).to.have.property('recursiveProp',
|
|
539
|
+
'[Circular]');
|
|
540
|
+
done();
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('honors rootProperty', function(done) {
|
|
545
|
+
givenErrorHandlerForError('Error Message', {rootProperty: 'data'});
|
|
546
|
+
requestJson().expect(500).end(function(err, res) {
|
|
547
|
+
if (err) return done(err);
|
|
548
|
+
|
|
549
|
+
expect(res.body.data).to.eql({
|
|
550
|
+
statusCode: 500,
|
|
551
|
+
message: 'Internal Server Error',
|
|
552
|
+
});
|
|
553
|
+
done();
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('honors rootProperty=false', function(done) {
|
|
558
|
+
givenErrorHandlerForError('Error Message', {rootProperty: false});
|
|
559
|
+
requestJson().expect(500).end(function(err, res) {
|
|
560
|
+
if (err) return done(err);
|
|
561
|
+
|
|
562
|
+
expect(res.body).to.eql({
|
|
563
|
+
statusCode: 500,
|
|
564
|
+
message: 'Internal Server Error',
|
|
565
|
+
});
|
|
566
|
+
done();
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
function requestJson(url) {
|
|
571
|
+
return request.get(url || '/')
|
|
572
|
+
.set('Accept', 'text/plain')
|
|
573
|
+
.expect('Content-Type', /^application\/json/);
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
context('HTML response', function() {
|
|
578
|
+
it('contains all error properties when debug=true', function(done) {
|
|
579
|
+
const error = new ErrorWithProps({
|
|
580
|
+
message: 'a test error message',
|
|
581
|
+
details: 'some details',
|
|
582
|
+
extra: 'sensitive data',
|
|
583
|
+
});
|
|
584
|
+
error.statusCode = 500;
|
|
585
|
+
givenErrorHandlerForError(error, {debug: true});
|
|
586
|
+
requestHTML()
|
|
587
|
+
.expect(500)
|
|
588
|
+
.expect(/<title>ErrorWithProps<\/title>/)
|
|
589
|
+
.expect(/500(.*?)a test error message/)
|
|
590
|
+
.expect(/extra(.*?)sensitive data/)
|
|
591
|
+
.expect(/details(.*?)some details/)
|
|
592
|
+
.expect(/id="stacktrace"(.*?)ErrorWithProps: a test error message/,
|
|
593
|
+
done);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it('HTML-escapes all 4xx response properties in production mode',
|
|
597
|
+
function(done) {
|
|
598
|
+
const error = new ErrorWithProps({
|
|
599
|
+
name: 'Error<img onerror=alert(1) src=a>',
|
|
600
|
+
message:
|
|
601
|
+
'No instance with id <img onerror=alert(1) src=a> found for Model',
|
|
602
|
+
statusCode: 404,
|
|
603
|
+
});
|
|
604
|
+
givenErrorHandlerForError(error, {debug: false});
|
|
605
|
+
requestHTML()
|
|
606
|
+
.end(function(err, res) {
|
|
607
|
+
expect(res.statusCode).to.eql(404);
|
|
608
|
+
const body = res.error.text;
|
|
609
|
+
expect(body).to.match(
|
|
610
|
+
/<title>Error<img onerror=alert\(1\) src=a><\/title>/,
|
|
611
|
+
);
|
|
612
|
+
expect(body).to.match(
|
|
613
|
+
/with id <img onerror=alert\(1\) src=a> found for Model/,
|
|
614
|
+
);
|
|
615
|
+
done();
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
it('HTML-escapes all 5xx response properties in development mode',
|
|
620
|
+
function(done) {
|
|
621
|
+
const error = new ErrorWithProps({
|
|
622
|
+
message: 'a test error message<img onerror=alert(1) src=a>',
|
|
623
|
+
});
|
|
624
|
+
error.statusCode = 500;
|
|
625
|
+
givenErrorHandlerForError(error, {debug: true});
|
|
626
|
+
requestHTML()
|
|
627
|
+
.expect(500)
|
|
628
|
+
.expect(/<title>ErrorWithProps<\/title>/)
|
|
629
|
+
.expect(
|
|
630
|
+
/500(.*?)a test error message<img onerror=alert\(1\) src=a>/,
|
|
631
|
+
done,
|
|
632
|
+
);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it('contains subset of properties when status=4xx', function(done) {
|
|
636
|
+
const error = new ErrorWithProps({
|
|
637
|
+
name: 'ValidationError',
|
|
638
|
+
message: 'The model instance is not valid.',
|
|
639
|
+
statusCode: 422,
|
|
640
|
+
details: 'some details',
|
|
641
|
+
extra: 'sensitive data',
|
|
642
|
+
});
|
|
643
|
+
givenErrorHandlerForError(error, {debug: false});
|
|
644
|
+
requestHTML()
|
|
645
|
+
.end(function(err, res) {
|
|
646
|
+
expect(res.statusCode).to.eql(422);
|
|
647
|
+
const body = res.error.text;
|
|
648
|
+
expect(body).to.match(/some details/);
|
|
649
|
+
expect(body).to.not.match(/sensitive data/);
|
|
650
|
+
expect(body).to.match(/<title>ValidationError<\/title>/);
|
|
651
|
+
expect(body).to.match(/422(.*?)The model instance is not valid./);
|
|
652
|
+
done();
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it('contains only safe info when status=5xx', function(done) {
|
|
657
|
+
// Mock an error reported by fs.readFile
|
|
658
|
+
const error = new ErrorWithProps({
|
|
659
|
+
name: 'Error',
|
|
660
|
+
message: 'ENOENT: no such file or directory, open "/etc/passwd"',
|
|
661
|
+
errno: -2,
|
|
662
|
+
code: 'ENOENT',
|
|
663
|
+
syscall: 'open',
|
|
664
|
+
path: '/etc/password',
|
|
665
|
+
});
|
|
666
|
+
givenErrorHandlerForError(error);
|
|
667
|
+
|
|
668
|
+
requestHTML()
|
|
669
|
+
.end(function(err, res) {
|
|
670
|
+
expect(res.statusCode).to.eql(500);
|
|
671
|
+
const body = res.error.text;
|
|
672
|
+
expect(body).to.not.match(/\/etc\/password/);
|
|
673
|
+
expect(body).to.not.match(/-2/);
|
|
674
|
+
expect(body).to.not.match(/ENOENT/);
|
|
675
|
+
// only have the following
|
|
676
|
+
expect(body).to.match(/<title>Internal Server Error<\/title>/);
|
|
677
|
+
expect(body).to.match(/500(.*?)Internal Server Error/);
|
|
678
|
+
done();
|
|
679
|
+
});
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
function requestHTML(url) {
|
|
683
|
+
return request.get(url || '/')
|
|
684
|
+
.set('Accept', 'text/html')
|
|
685
|
+
.expect('Content-Type', /^text\/html/);
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
context('XML response', function() {
|
|
690
|
+
it('contains all error properties when debug=true', function(done) {
|
|
691
|
+
const error = new ErrorWithProps({
|
|
692
|
+
message: 'a test error message',
|
|
693
|
+
details: 'some details',
|
|
694
|
+
extra: 'sensitive data',
|
|
695
|
+
});
|
|
696
|
+
error.statusCode = 500;
|
|
697
|
+
givenErrorHandlerForError(error, {debug: true});
|
|
698
|
+
requestXML()
|
|
699
|
+
.expect(500)
|
|
700
|
+
.expect(/<statusCode>500<\/statusCode>/)
|
|
701
|
+
.expect(/<name>ErrorWithProps<\/name>/)
|
|
702
|
+
.expect(/<message>a test error message<\/message>/)
|
|
703
|
+
.expect(/<details>some details<\/details>/)
|
|
704
|
+
.expect(/<extra>sensitive data<\/extra>/)
|
|
705
|
+
.expect(/<stack>ErrorWithProps: a test error message(.*?)/, done);
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
it('contains subset of properties when status=4xx', function(done) {
|
|
709
|
+
const error = new ErrorWithProps({
|
|
710
|
+
name: 'ValidationError',
|
|
711
|
+
message: 'The model instance is not valid.',
|
|
712
|
+
statusCode: 422,
|
|
713
|
+
details: 'some details',
|
|
714
|
+
extra: 'sensitive data',
|
|
715
|
+
});
|
|
716
|
+
givenErrorHandlerForError(error, {debug: false});
|
|
717
|
+
requestXML()
|
|
718
|
+
.end(function(err, res) {
|
|
719
|
+
expect(res.statusCode).to.eql(422);
|
|
720
|
+
const body = res.error.text;
|
|
721
|
+
expect(body).to.match(/<details>some details<\/details>/);
|
|
722
|
+
expect(body).to.not.match(/<extra>sensitive data<\/extra>/);
|
|
723
|
+
expect(body).to.match(/<name>ValidationError<\/name>/);
|
|
724
|
+
expect(body).to.match(
|
|
725
|
+
/<message>The model instance is not valid.<\/message>/,
|
|
726
|
+
);
|
|
727
|
+
done();
|
|
728
|
+
});
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
it('contains only safe info when status=5xx', function(done) {
|
|
732
|
+
// Mock an error reported by fs.readFile
|
|
733
|
+
const error = new ErrorWithProps({
|
|
734
|
+
name: 'Error',
|
|
735
|
+
message: 'ENOENT: no such file or directory, open "/etc/passwd"',
|
|
736
|
+
errno: -2,
|
|
737
|
+
code: 'ENOENT',
|
|
738
|
+
syscall: 'open',
|
|
739
|
+
path: '/etc/password',
|
|
740
|
+
});
|
|
741
|
+
givenErrorHandlerForError(error);
|
|
742
|
+
|
|
743
|
+
requestXML()
|
|
744
|
+
.end(function(err, res) {
|
|
745
|
+
expect(res.statusCode).to.eql(500);
|
|
746
|
+
const body = res.error.text;
|
|
747
|
+
expect(body).to.not.match(/\/etc\/password/);
|
|
748
|
+
expect(body).to.not.match(/-2/);
|
|
749
|
+
expect(body).to.not.match(/ENOENT/);
|
|
750
|
+
// only have the following
|
|
751
|
+
expect(body).to.match(/<statusCode>500<\/statusCode>/);
|
|
752
|
+
expect(body).to.match(/<message>Internal Server Error<\/message>/);
|
|
753
|
+
done();
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
it('honors options.rootProperty', function(done) {
|
|
758
|
+
const error = new ErrorWithProps({
|
|
759
|
+
name: 'ValidationError',
|
|
760
|
+
message: 'The model instance is not valid.',
|
|
761
|
+
statusCode: 422,
|
|
762
|
+
details: 'some details',
|
|
763
|
+
extra: 'sensitive data',
|
|
764
|
+
});
|
|
765
|
+
givenErrorHandlerForError(error, {rootProperty: 'myRoot'});
|
|
766
|
+
requestXML()
|
|
767
|
+
.end(function(err, res) {
|
|
768
|
+
expect(res.statusCode).to.eql(422);
|
|
769
|
+
const body = res.error.text;
|
|
770
|
+
expect(body).to.match(/<myRoot>/);
|
|
771
|
+
expect(body).to.match(/<details>some details<\/details>/);
|
|
772
|
+
expect(body).to.not.match(/<extra>sensitive data<\/extra>/);
|
|
773
|
+
expect(body).to.match(/<name>ValidationError<\/name>/);
|
|
774
|
+
expect(body).to.match(
|
|
775
|
+
/<message>The model instance is not valid.<\/message>/,
|
|
776
|
+
);
|
|
777
|
+
done();
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
it('ignores options.rootProperty = false', function(done) {
|
|
782
|
+
const error = new ErrorWithProps({
|
|
783
|
+
name: 'ValidationError',
|
|
784
|
+
message: 'The model instance is not valid.',
|
|
785
|
+
statusCode: 422,
|
|
786
|
+
details: 'some details',
|
|
787
|
+
extra: 'sensitive data',
|
|
788
|
+
});
|
|
789
|
+
givenErrorHandlerForError(error, {rootProperty: false});
|
|
790
|
+
requestXML()
|
|
791
|
+
.end(function(err, res) {
|
|
792
|
+
expect(res.statusCode).to.eql(422);
|
|
793
|
+
const body = res.error.text;
|
|
794
|
+
expect(body).to.match(/<error>/);
|
|
795
|
+
expect(body).to.match(/<details>some details<\/details>/);
|
|
796
|
+
expect(body).to.not.match(/<extra>sensitive data<\/extra>/);
|
|
797
|
+
expect(body).to.match(/<name>ValidationError<\/name>/);
|
|
798
|
+
expect(body).to.match(
|
|
799
|
+
/<message>The model instance is not valid.<\/message>/,
|
|
800
|
+
);
|
|
801
|
+
done();
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
function requestXML(url) {
|
|
806
|
+
return request.get(url || '/')
|
|
807
|
+
.set('Accept', 'text/xml')
|
|
808
|
+
.expect('Content-Type', /^text\/xml/);
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
context('Content Negotiation', function() {
|
|
813
|
+
it('defaults to json without options', function(done) {
|
|
814
|
+
givenErrorHandlerForError(new Error('Some error'), {});
|
|
815
|
+
request.get('/')
|
|
816
|
+
.set('Accept', '*/*')
|
|
817
|
+
.expect('Content-Type', /^application\/json/, done);
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
it('honors accepted content-type', function(done) {
|
|
821
|
+
givenErrorHandlerForError(new Error('Some error'), {
|
|
822
|
+
defaultType: 'application/json',
|
|
823
|
+
});
|
|
824
|
+
request.get('/')
|
|
825
|
+
.set('Accept', 'text/html')
|
|
826
|
+
.expect('Content-Type', /^text\/html/, done);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
it('honors order of accepted content-type', function(done) {
|
|
830
|
+
givenErrorHandlerForError(new Error('Some error'), {
|
|
831
|
+
defaultType: 'text/html',
|
|
832
|
+
});
|
|
833
|
+
request.get('/')
|
|
834
|
+
// `application/json` will be used because its provided first
|
|
835
|
+
.set('Accept', 'application/json, text/html')
|
|
836
|
+
.expect('Content-Type', /^application\/json/, done);
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
it('disables content-type negotiation when negotiateContentType=false',
|
|
840
|
+
function(done) {
|
|
841
|
+
givenErrorHandlerForError(new Error('Some error'), {
|
|
842
|
+
negotiateContentType: false,
|
|
843
|
+
defaultType: 'application/json',
|
|
844
|
+
});
|
|
845
|
+
request.get('/')
|
|
846
|
+
.set('Accept', 'text/html')
|
|
847
|
+
.expect('Content-Type', /^application\/json/, done);
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
it('chooses resolved type when negotiateContentType=false + not-supported',
|
|
851
|
+
function(done) {
|
|
852
|
+
givenErrorHandlerForError(new Error('Some error'), {
|
|
853
|
+
negotiateContentType: false,
|
|
854
|
+
defaultType: 'unsupported/type',
|
|
855
|
+
});
|
|
856
|
+
request.get('/')
|
|
857
|
+
.set('Accept', 'text/html')
|
|
858
|
+
.expect('Content-Type', /^text\/html/, done);
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
it('chooses default type when negotiateContentType=false + not-supported ',
|
|
862
|
+
function(done) {
|
|
863
|
+
givenErrorHandlerForError(new Error('Some error'), {
|
|
864
|
+
negotiateContentType: false,
|
|
865
|
+
defaultType: 'unsupported/type',
|
|
866
|
+
});
|
|
867
|
+
request.get('/')
|
|
868
|
+
.expect('Content-Type', /^application\/json/, done);
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
it('honors order of accepted content-types of text/html', function(done) {
|
|
872
|
+
givenErrorHandlerForError(new Error('Some error'), {
|
|
873
|
+
defaultType: 'application/json',
|
|
874
|
+
});
|
|
875
|
+
request.get('/')
|
|
876
|
+
// text/html will be used because its provided first
|
|
877
|
+
.set('Accept', 'text/html, application/json')
|
|
878
|
+
.expect('Content-Type', /^text\/html/, done);
|
|
879
|
+
});
|
|
880
|
+
|
|
881
|
+
it('picks first supported type upon multiple accepted', function(done) {
|
|
882
|
+
givenErrorHandlerForError(new Error('Some error'), {
|
|
883
|
+
defaultType: 'application/json',
|
|
884
|
+
});
|
|
885
|
+
request.get('/')
|
|
886
|
+
.set('Accept', '*/*, not-supported, text/html, application/json')
|
|
887
|
+
.expect('Content-Type', /^text\/html/, done);
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
it('falls back for unsupported option.defaultType', function(done) {
|
|
891
|
+
givenErrorHandlerForError(new Error('Some error'), {
|
|
892
|
+
defaultType: 'unsupported',
|
|
893
|
+
});
|
|
894
|
+
request.get('/')
|
|
895
|
+
.set('Accept', '*/*')
|
|
896
|
+
.expect('Content-Type', /^application\/json/, done);
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
it('returns defaultType for unsupported type', function(done) {
|
|
900
|
+
givenErrorHandlerForError(new Error('Some error'), {
|
|
901
|
+
defaultType: 'text/html',
|
|
902
|
+
});
|
|
903
|
+
request.get('/')
|
|
904
|
+
.set('Accept', 'unsupported/type')
|
|
905
|
+
.expect('Content-Type', /^text\/html/, done);
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
it('supports query _format', function(done) {
|
|
909
|
+
givenErrorHandlerForError(new Error('Some error'), {
|
|
910
|
+
defaultType: 'text/html',
|
|
911
|
+
});
|
|
912
|
+
request.get('/?_format=html')
|
|
913
|
+
.set('Accept', 'application/json')
|
|
914
|
+
.expect('Content-Type', /^text\/html/, done);
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
it('handles unknown _format query', function() {
|
|
918
|
+
givenErrorHandlerForError();
|
|
919
|
+
return request.get('/?_format=unknown')
|
|
920
|
+
.expect('X-Warning', /_format.*not supported/);
|
|
921
|
+
});
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
it('does not modify "options" argument', function(done) {
|
|
925
|
+
const options = {log: false, debug: false};
|
|
926
|
+
givenErrorHandlerForError(new Error(), options);
|
|
927
|
+
request.get('/').end(function(err) {
|
|
928
|
+
if (err) return done(err);
|
|
929
|
+
expect(options).to.eql({log: false, debug: false});
|
|
930
|
+
done();
|
|
931
|
+
});
|
|
932
|
+
});
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
let app, _requestHandler, request, server;
|
|
936
|
+
function resetRequestHandler() {
|
|
937
|
+
_requestHandler = null;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function givenErrorHandlerForError(error, options) {
|
|
941
|
+
if (!error) error = new Error('an error');
|
|
942
|
+
|
|
943
|
+
if (!options) options = {};
|
|
944
|
+
if (!('log' in options)) {
|
|
945
|
+
// Disable logging to console by default, so that we don't spam
|
|
946
|
+
// console output. One can use "DEBUG=strong-error-handler" when
|
|
947
|
+
// troubleshooting.
|
|
948
|
+
options.log = false;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const handler = strongErrorHandler(options);
|
|
952
|
+
_requestHandler = function(req, res, next) {
|
|
953
|
+
debug('Invoking strong-error-handler');
|
|
954
|
+
handler(error, req, res, next);
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function setupHttpServerAndClient(done) {
|
|
959
|
+
app = express();
|
|
960
|
+
app.use(function(req, res, next) {
|
|
961
|
+
if (!_requestHandler) {
|
|
962
|
+
const msg = 'Error handler middleware was not setup in this test';
|
|
963
|
+
console.error(msg);
|
|
964
|
+
res.statusCode = 500;
|
|
965
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
966
|
+
res.end(msg);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
_requestHandler(req, res, warnUnhandledError);
|
|
971
|
+
|
|
972
|
+
function warnUnhandledError(err) {
|
|
973
|
+
console.log('unexpected: strong-error-handler called next with',
|
|
974
|
+
(err && (err.stack || err)) || 'no error');
|
|
975
|
+
res.statusCode = 500;
|
|
976
|
+
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
977
|
+
res.end(err ?
|
|
978
|
+
'Unhandled strong-error-handler error:\n' + (err.stack || err) :
|
|
979
|
+
'The error was silently discared by strong-error-handler');
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
server = app.listen(0, function() {
|
|
984
|
+
const url = 'http://127.0.0.1:' + this.address().port;
|
|
985
|
+
debug('Test server listening on %s', url);
|
|
986
|
+
request = supertest(app);
|
|
987
|
+
done();
|
|
988
|
+
})
|
|
989
|
+
.once('error', function(err) {
|
|
990
|
+
debug('Cannot setup HTTP server: %s', err.stack);
|
|
991
|
+
done(err);
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function stopHttpServerAndClient() {
|
|
996
|
+
server.close();
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function ErrorWithProps(props) {
|
|
1000
|
+
this.name = props.name || 'ErrorWithProps';
|
|
1001
|
+
for (const p in props) {
|
|
1002
|
+
this[p] = props[p];
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
if (Error.captureStackTrace) {
|
|
1006
|
+
// V8 (Chrome, Opera, Node)
|
|
1007
|
+
Error.captureStackTrace(this, this.constructor);
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
util.inherits(ErrorWithProps, Error);
|
|
1011
|
+
|
|
1012
|
+
function getExpectedErrorData(err) {
|
|
1013
|
+
const data = {};
|
|
1014
|
+
cloneAllProperties(data, err);
|
|
1015
|
+
return data;
|
|
1016
|
+
}
|