strong-error-handler 3.5.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 ADDED
@@ -0,0 +1,6 @@
1
+ sudo: false
2
+ language: node_js
3
+ node_js:
4
+ - "10"
5
+ - "12"
6
+ - "14"
package/CHANGES.md CHANGED
@@ -1,3 +1,11 @@
1
+ 2020-10-13, Version 4.0.0
2
+ =========================
3
+
4
+ * docs: update LTS versions in README (Miroslav Bajtoš)
5
+
6
+ * [SEMVER-MAJOR] Reword log messages for clarity (Miroslav Bajtoš)
7
+
8
+
1
9
  2020-06-23, Version 3.5.0
2
10
  =========================
3
11
 
package/README.md CHANGED
@@ -14,11 +14,15 @@ In debug mode, `strong-error-handler` returns full error stack traces and intern
14
14
 
15
15
  ## Supported versions
16
16
 
17
- | Current | Long Term Support | Maintenance |
18
- | :-----: | :---------------: | :---------: |
19
- | 4.x | 3.x | 2.x |
17
+ This module adopts the [Module Long Term Support (LTS)](http://github.com/CloudNativeJS/ModuleLTS) policy, with the following End Of Life (EOL) dates:
20
18
 
21
- Learn more about our LTS plan in [docs](http://loopback.io/doc/en/contrib/Long-term-support.html).
19
+ | Version | Status | Published | EOL |
20
+ | ---------- | --------------- | --------- | -------------------- |
21
+ | 4.x | Current | Oct 2020 | Apr 2023 _(minimum)_ |
22
+ | 3.x | Active LTS | Jun 2018 | Dec 2022 |
23
+ | 2.x | End-of-life | Mar 2017 | Oct 2020 |
24
+
25
+ Learn more about our LTS plan in the [LoopBack documentation](http://loopback.io/doc/en/contrib/Long-term-support.html).
22
26
 
23
27
  ## Installation
24
28
 
package/lib/logger.js CHANGED
@@ -10,12 +10,12 @@ const g = require('strong-globalize')();
10
10
 
11
11
  module.exports = function logToConsole(req, err) {
12
12
  if (!Array.isArray(err)) {
13
- g.error('Unhandled error for request %s %s: %s',
13
+ g.error('Request %s %s failed: %s',
14
14
  req.method, req.url, err.stack || err);
15
15
  return;
16
16
  }
17
17
 
18
- const errMsg = g.f('Unhandled array of errors for request %s %s\n',
18
+ const errMsg = g.f('Request %s %s failed with multiple errors:\n',
19
19
  req.method, req.url);
20
20
  const errors = err.map(formatError).join('\n');
21
21
  console.error(errMsg, errors);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "strong-error-handler",
3
3
  "description": "Error handler for use in development and production environments.",
4
4
  "license": "MIT",
5
- "version": "3.5.0",
5
+ "version": "4.0.0",
6
6
  "engines": {
7
7
  "node": ">=10"
8
8
  },
@@ -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&lt;img onerror=alert\(1\) src=a&gt;<\/title>/,
611
+ );
612
+ expect(body).to.match(
613
+ /with id &lt;img onerror=alert\(1\) src=a&gt; 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&lt;img onerror=alert\(1\) src=a&gt;/,
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
+ }