strong-error-handler 3.4.0 → 3.5.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/CHANGES.md CHANGED
@@ -1,3 +1,11 @@
1
+ 2020-06-23, Version 3.5.0
2
+ =========================
3
+
4
+ * feat: add options.rootProperty for json/xml (Raymond Feng)
5
+
6
+ * chore: update deps and drop Node 8.x support (Raymond Feng)
7
+
8
+
1
9
  2019-10-12, Version 3.4.0
2
10
  =========================
3
11
 
package/README.md CHANGED
@@ -14,9 +14,9 @@ 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
- 3.x|2.x|1.x
17
+ | Current | Long Term Support | Maintenance |
18
+ | :-----: | :---------------: | :---------: |
19
+ | 4.x | 3.x | 2.x |
20
20
 
21
21
  Learn more about our LTS plan in [docs](http://loopback.io/doc/en/contrib/Long-term-support.html).
22
22
 
@@ -108,24 +108,25 @@ The content type of the response depends on the request's `Accepts` header.
108
108
 
109
109
  ## Options
110
110
 
111
- | Option | Type | Default | Description |
112
- | ---- | ---- | ---- | ---- |
113
- | debug | Boolean    | `false` | If `true`, HTTP responses include all error properties, including sensitive data such as file paths, URLs and stack traces. See [Example output](#example) below. |
114
- | log | Boolean | `true` | If `true`, all errors are printed via `console.error`, including an array of fields (custom error properties) that are safe to include in response messages (both 4xx and 5xx). <br/> If `false`, sends only the error back in the response. |
115
- | safeFields | [String] | `[]` | Specifies property names on errors that are allowed to be passed through in 4xx and 5xx responses. See [Safe error fields](#safe-error-fields) below. |
116
- | defaultType | String | `"json"` | Specify the default response content type to use when the client does not provide any Accepts header.
117
- | negotiateContentType | Boolean | true | Negotiate the response content type via Accepts request header. When disabled, strong-error-handler will always use the default content type when producing responses. Disabling content type negotiation is useful if you want to see JSON-formatted error responses in browsers, because browsers usually prefer HTML and XML over other content types.
111
+ | Option | Type | Default | Description |
112
+ | -------------------- | ------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
113
+ | debug | Boolean&nbsp;&nbsp;&nbsp; | `false` | If `true`, HTTP responses include all error properties, including sensitive data such as file paths, URLs and stack traces. See [Example output](#example) below. |
114
+ | log | Boolean | `true` | If `true`, all errors are printed via `console.error`, including an array of fields (custom error properties) that are safe to include in response messages (both 4xx and 5xx). <br/> If `false`, sends only the error back in the response. |
115
+ | safeFields | [String] | `[]` | Specifies property names on errors that are allowed to be passed through in 4xx and 5xx responses. See [Safe error fields](#safe-error-fields) below. |
116
+ | defaultType | String | `"json"` | Specifies the default response content type to use when the client does not provide any Accepts header. |
117
+ | rootProperty | String or false | `"error"` | Specifies the root property name for json or xml. If the value is set to `false`, no wrapper will be added to the json object. The false value is ignored by XML as a root element is always required. |
118
+ | negotiateContentType | Boolean | true | Negotiate the response content type via Accepts request header. When disabled, strong-error-handler will always use the default content type when producing responses. Disabling content type negotiation is useful if you want to see JSON-formatted error responses in browsers, because browsers usually prefer HTML and XML over other content types. |
118
119
 
119
120
  ### Customizing log format
120
121
 
121
- **Express**
122
+ **Express**
122
123
 
123
- To use a different log format, add your own custom error-handling middleware then disable `errorHandler.log`.
124
+ To use a different log format, add your own custom error-handling middleware then disable `errorHandler.log`.
124
125
  For example, in an Express application:
125
126
 
126
127
  ```js
127
128
  app.use(myErrorLogger());
128
- app.use(errorHandler({ log: false }));
129
+ app.use(errorHandler({log: false}));
129
130
  ```
130
131
 
131
132
  In general, add `strong-error-handler` as the last middleware function, just before calling `app.listen()`.
@@ -234,7 +235,7 @@ To migrate a LoopBack 2.x application to use `strong-error-handler`:
234
235
  }
235
236
  </pre>
236
237
 
237
- For more information, see
238
+ For more information, see
238
239
  [Migrating apps to LoopBack 3.0](http://loopback.io/doc/en/lb3/Migrating-to-3.0.html#update-use-of-rest-error-handler).
239
240
 
240
241
  ## Example
@@ -252,17 +253,17 @@ The same error generated when `debug: true` :
252
253
  { statusCode: 500,
253
254
  name: 'Error',
254
255
  message: 'a test error message',
255
- stack: 'Error: a test error message
256
- at Context.<anonymous> (User/strong-error-handler/test/handler.test.js:220:21)
257
- at callFnAsync (User/strong-error-handler/node_modules/mocha/lib/runnable.js:349:8)
258
- at Test.Runnable.run (User/strong-error-handler/node_modules/mocha/lib/runnable.js:301:7)
259
- at Runner.runTest (User/strong-error-handler/node_modules/mocha/lib/runner.js:422:10)
260
- at User/strong-error-handler/node_modules/mocha/lib/runner.js:528:12
261
- at next (User/strong-error-handler/node_modules/mocha/lib/runner.js:342:14)
262
- at User/strong-error-handler/node_modules/mocha/lib/runner.js:352:7
263
- at next (User/strong-error-handler/node_modules/mocha/lib/runner.js:284:14)
264
- at Immediate._onImmediate (User/strong-error-handler/node_modules/mocha/lib/runner.js:320:5)
265
- at tryOnImmediate (timers.js:543:15)
256
+ stack: 'Error: a test error message
257
+ at Context.<anonymous> (User/strong-error-handler/test/handler.test.js:220:21)
258
+ at callFnAsync (User/strong-error-handler/node_modules/mocha/lib/runnable.js:349:8)
259
+ at Test.Runnable.run (User/strong-error-handler/node_modules/mocha/lib/runnable.js:301:7)
260
+ at Runner.runTest (User/strong-error-handler/node_modules/mocha/lib/runner.js:422:10)
261
+ at User/strong-error-handler/node_modules/mocha/lib/runner.js:528:12
262
+ at next (User/strong-error-handler/node_modules/mocha/lib/runner.js:342:14)
263
+ at User/strong-error-handler/node_modules/mocha/lib/runner.js:352:7
264
+ at next (User/strong-error-handler/node_modules/mocha/lib/runner.js:284:14)
265
+ at Immediate._onImmediate (User/strong-error-handler/node_modules/mocha/lib/runner.js:320:5)
266
+ at tryOnImmediate (timers.js:543:15)
266
267
  at processImmediate [as _immediateCallback] (timers.js:523:5)' }}
267
268
  ```
268
269
 
package/index.d.ts CHANGED
@@ -53,6 +53,7 @@ declare namespace errorHandlerFactory {
53
53
  safeFields?: string[];
54
54
  defaultType?: string;
55
55
  negotiateContentType?: boolean;
56
+ rootProperty?: string | false;
56
57
  }
57
58
 
58
59
  /**
package/lib/handler.js CHANGED
@@ -10,7 +10,6 @@ const SG = require('strong-globalize');
10
10
  SG.SetRootDir(path.resolve(__dirname, '..'));
11
11
  const buildResponseData = require('./data-builder');
12
12
  const debug = require('debug')('strong-error-handler');
13
- const format = require('util').format;
14
13
  const logToConsole = require('./logger');
15
14
  const negotiateContentProducer = require('./content-negotiation');
16
15
 
@@ -50,7 +49,7 @@ function writeErrorToResponse(err, req, res, options) {
50
49
 
51
50
  options = options || {};
52
51
 
53
- if (res._header) {
52
+ if (res.headersSent) {
54
53
  debug('Response was already sent, closing the underlying connection');
55
54
  return req.socket.destroy();
56
55
  }
@@ -66,7 +65,7 @@ function writeErrorToResponse(err, req, res, options) {
66
65
  res.statusCode = data.statusCode;
67
66
 
68
67
  const sendResponse = negotiateContentProducer(req, warn, options);
69
- sendResponse(res, data);
68
+ sendResponse(res, data, options);
70
69
 
71
70
  function warn(msg) {
72
71
  res.header('X-Warning', msg);
package/lib/send-html.js CHANGED
@@ -17,10 +17,10 @@ const compiledTemplates = {
17
17
  module.exports = sendHtml;
18
18
 
19
19
  function sendHtml(res, data, options) {
20
- const toRender = {options: {}, data: data};
20
+ const toRender = {options, data};
21
21
  // TODO: ability to call non-default template functions from options
22
22
  const body = compiledTemplates.default(toRender);
23
- sendReponse(res, body);
23
+ sendResponse(res, body);
24
24
  }
25
25
 
26
26
  /**
@@ -41,7 +41,7 @@ function loadDefaultTemplates() {
41
41
  return compileTemplate(defaultTemplate);
42
42
  }
43
43
 
44
- function sendReponse(res, body) {
44
+ function sendResponse(res, body) {
45
45
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
46
46
  res.end(body);
47
47
  }
package/lib/send-json.js CHANGED
@@ -7,8 +7,14 @@
7
7
 
8
8
  const safeStringify = require('fast-safe-stringify');
9
9
 
10
- module.exports = function sendJson(res, data) {
11
- const content = safeStringify({error: data});
10
+ module.exports = function sendJson(res, data, options) {
11
+ options = options || {};
12
+ // Set `options.rootProperty` to not wrap the data into an `error` object
13
+ const err = options.rootProperty === false ? data : {
14
+ // Use `options.rootProperty`, if not set, default to `error`
15
+ [options.rootProperty || 'error']: data,
16
+ };
17
+ const content = safeStringify(err);
12
18
  res.setHeader('Content-Type', 'application/json; charset=utf-8');
13
19
  res.end(content, 'utf-8');
14
20
  };
package/lib/send-xml.js CHANGED
@@ -7,8 +7,12 @@
7
7
 
8
8
  const js2xmlparser = require('js2xmlparser');
9
9
 
10
- module.exports = function sendXml(res, data) {
11
- const content = js2xmlparser.parse('error', data);
10
+ module.exports = function sendXml(res, data, options) {
11
+ options = options || {};
12
+ // Xml always requires a root element.
13
+ // `options.rootProperty === false` is not honored
14
+ const root = options.rootProperty || 'error';
15
+ const content = js2xmlparser.parse(root, data);
12
16
  res.setHeader('Content-Type', 'text/xml; charset=utf-8');
13
17
  res.end(content, 'utf-8');
14
18
  };
package/package.json CHANGED
@@ -2,9 +2,9 @@
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.4.0",
5
+ "version": "3.5.0",
6
6
  "engines": {
7
- "node": ">=8"
7
+ "node": ">=10"
8
8
  },
9
9
  "repository": {
10
10
  "type": "git",
@@ -20,18 +20,18 @@
20
20
  "@types/express": "^4.16.0",
21
21
  "accepts": "^1.3.3",
22
22
  "debug": "^4.1.1",
23
- "ejs": "^2.6.1",
23
+ "ejs": "^3.1.3",
24
24
  "fast-safe-stringify": "^2.0.6",
25
25
  "http-status": "^1.1.2",
26
26
  "js2xmlparser": "^4.0.0",
27
- "strong-globalize": "^5.0.2"
27
+ "strong-globalize": "^6.0.1"
28
28
  },
29
29
  "devDependencies": {
30
30
  "chai": "^4.1.2",
31
- "eslint": "^6.5.1",
31
+ "eslint": "^7.0.0",
32
32
  "eslint-config-loopback": "^13.1.0",
33
33
  "express": "^4.16.3",
34
- "mocha": "^6.2.1",
34
+ "mocha": "^7.1.2",
35
35
  "supertest": "^4.0.2"
36
36
  },
37
37
  "browser": {
@@ -2,7 +2,7 @@
2
2
  <head>
3
3
  <meta charset='utf-8'>
4
4
  <title><%= data.name || data.message %></title>
5
- <style><%- include style.css %></style>
5
+ <style><%- include('style.css') %></style>
6
6
  </head>
7
7
  <body>
8
8
  <div id="wrapper">
package/.travis.yml DELETED
@@ -1,6 +0,0 @@
1
- sudo: false
2
- language: node_js
3
- node_js:
4
- - "8"
5
- - "10"
6
- - "12"
@@ -1,942 +0,0 @@
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
- function requestJson(url) {
545
- return request.get(url || '/')
546
- .set('Accept', 'text/plain')
547
- .expect('Content-Type', /^application\/json/);
548
- }
549
- });
550
-
551
- context('HTML response', function() {
552
- it('contains all error properties when debug=true', function(done) {
553
- const error = new ErrorWithProps({
554
- message: 'a test error message',
555
- details: 'some details',
556
- extra: 'sensitive data',
557
- });
558
- error.statusCode = 500;
559
- givenErrorHandlerForError(error, {debug: true});
560
- requestHTML()
561
- .expect(500)
562
- .expect(/<title>ErrorWithProps<\/title>/)
563
- .expect(/500(.*?)a test error message/)
564
- .expect(/extra(.*?)sensitive data/)
565
- .expect(/details(.*?)some details/)
566
- .expect(/id="stacktrace"(.*?)ErrorWithProps: a test error message/,
567
- done);
568
- });
569
-
570
- it('HTML-escapes all 4xx response properties in production mode',
571
- function(done) {
572
- const error = new ErrorWithProps({
573
- name: 'Error<img onerror=alert(1) src=a>',
574
- message:
575
- 'No instance with id <img onerror=alert(1) src=a> found for Model',
576
- statusCode: 404,
577
- });
578
- givenErrorHandlerForError(error, {debug: false});
579
- requestHTML()
580
- .end(function(err, res) {
581
- expect(res.statusCode).to.eql(404);
582
- const body = res.error.text;
583
- expect(body).to.match(
584
- /<title>Error&lt;img onerror=alert\(1\) src=a&gt;<\/title>/
585
- );
586
- expect(body).to.match(
587
- /with id &lt;img onerror=alert\(1\) src=a&gt; found for Model/
588
- );
589
- done();
590
- });
591
- });
592
-
593
- it('HTML-escapes all 5xx response properties in development mode',
594
- function(done) {
595
- const error = new ErrorWithProps({
596
- message: 'a test error message<img onerror=alert(1) src=a>',
597
- });
598
- error.statusCode = 500;
599
- givenErrorHandlerForError(error, {debug: true});
600
- requestHTML()
601
- .expect(500)
602
- .expect(/<title>ErrorWithProps<\/title>/)
603
- .expect(
604
- /500(.*?)a test error message&lt;img onerror=alert\(1\) src=a&gt;/,
605
- done
606
- );
607
- });
608
-
609
- it('contains subset of properties when status=4xx', function(done) {
610
- const error = new ErrorWithProps({
611
- name: 'ValidationError',
612
- message: 'The model instance is not valid.',
613
- statusCode: 422,
614
- details: 'some details',
615
- extra: 'sensitive data',
616
- });
617
- givenErrorHandlerForError(error, {debug: false});
618
- requestHTML()
619
- .end(function(err, res) {
620
- expect(res.statusCode).to.eql(422);
621
- const body = res.error.text;
622
- expect(body).to.match(/some details/);
623
- expect(body).to.not.match(/sensitive data/);
624
- expect(body).to.match(/<title>ValidationError<\/title>/);
625
- expect(body).to.match(/422(.*?)The model instance is not valid./);
626
- done();
627
- });
628
- });
629
-
630
- it('contains only safe info when status=5xx', function(done) {
631
- // Mock an error reported by fs.readFile
632
- const error = new ErrorWithProps({
633
- name: 'Error',
634
- message: 'ENOENT: no such file or directory, open "/etc/passwd"',
635
- errno: -2,
636
- code: 'ENOENT',
637
- syscall: 'open',
638
- path: '/etc/password',
639
- });
640
- givenErrorHandlerForError(error);
641
-
642
- requestHTML()
643
- .end(function(err, res) {
644
- expect(res.statusCode).to.eql(500);
645
- const body = res.error.text;
646
- expect(body).to.not.match(/\/etc\/password/);
647
- expect(body).to.not.match(/-2/);
648
- expect(body).to.not.match(/ENOENT/);
649
- // only have the following
650
- expect(body).to.match(/<title>Internal Server Error<\/title>/);
651
- expect(body).to.match(/500(.*?)Internal Server Error/);
652
- done();
653
- });
654
- });
655
-
656
- function requestHTML(url) {
657
- return request.get(url || '/')
658
- .set('Accept', 'text/html')
659
- .expect('Content-Type', /^text\/html/);
660
- }
661
- });
662
-
663
- context('XML response', function() {
664
- it('contains all error properties when debug=true', function(done) {
665
- const error = new ErrorWithProps({
666
- message: 'a test error message',
667
- details: 'some details',
668
- extra: 'sensitive data',
669
- });
670
- error.statusCode = 500;
671
- givenErrorHandlerForError(error, {debug: true});
672
- requestXML()
673
- .expect(500)
674
- .expect(/<statusCode>500<\/statusCode>/)
675
- .expect(/<name>ErrorWithProps<\/name>/)
676
- .expect(/<message>a test error message<\/message>/)
677
- .expect(/<details>some details<\/details>/)
678
- .expect(/<extra>sensitive data<\/extra>/)
679
- .expect(/<stack>ErrorWithProps: a test error message(.*?)/, done);
680
- });
681
-
682
- it('contains subset of properties when status=4xx', function(done) {
683
- const error = new ErrorWithProps({
684
- name: 'ValidationError',
685
- message: 'The model instance is not valid.',
686
- statusCode: 422,
687
- details: 'some details',
688
- extra: 'sensitive data',
689
- });
690
- givenErrorHandlerForError(error, {debug: false});
691
- requestXML()
692
- .end(function(err, res) {
693
- expect(res.statusCode).to.eql(422);
694
- const body = res.error.text;
695
- expect(body).to.match(/<details>some details<\/details>/);
696
- expect(body).to.not.match(/<extra>sensitive data<\/extra>/);
697
- expect(body).to.match(/<name>ValidationError<\/name>/);
698
- expect(body).to.match(
699
- /<message>The model instance is not valid.<\/message>/
700
- );
701
- done();
702
- });
703
- });
704
-
705
- it('contains only safe info when status=5xx', function(done) {
706
- // Mock an error reported by fs.readFile
707
- const error = new ErrorWithProps({
708
- name: 'Error',
709
- message: 'ENOENT: no such file or directory, open "/etc/passwd"',
710
- errno: -2,
711
- code: 'ENOENT',
712
- syscall: 'open',
713
- path: '/etc/password',
714
- });
715
- givenErrorHandlerForError(error);
716
-
717
- requestXML()
718
- .end(function(err, res) {
719
- expect(res.statusCode).to.eql(500);
720
- const body = res.error.text;
721
- expect(body).to.not.match(/\/etc\/password/);
722
- expect(body).to.not.match(/-2/);
723
- expect(body).to.not.match(/ENOENT/);
724
- // only have the following
725
- expect(body).to.match(/<statusCode>500<\/statusCode>/);
726
- expect(body).to.match(/<message>Internal Server Error<\/message>/);
727
- done();
728
- });
729
- });
730
-
731
- function requestXML(url) {
732
- return request.get(url || '/')
733
- .set('Accept', 'text/xml')
734
- .expect('Content-Type', /^text\/xml/);
735
- }
736
- });
737
-
738
- context('Content Negotiation', function() {
739
- it('defaults to json without options', function(done) {
740
- givenErrorHandlerForError(new Error('Some error'), {});
741
- request.get('/')
742
- .set('Accept', '*/*')
743
- .expect('Content-Type', /^application\/json/, done);
744
- });
745
-
746
- it('honors accepted content-type', function(done) {
747
- givenErrorHandlerForError(new Error('Some error'), {
748
- defaultType: 'application/json',
749
- });
750
- request.get('/')
751
- .set('Accept', 'text/html')
752
- .expect('Content-Type', /^text\/html/, done);
753
- });
754
-
755
- it('honors order of accepted content-type', function(done) {
756
- givenErrorHandlerForError(new Error('Some error'), {
757
- defaultType: 'text/html',
758
- });
759
- request.get('/')
760
- // `application/json` will be used because its provided first
761
- .set('Accept', 'application/json, text/html')
762
- .expect('Content-Type', /^application\/json/, done);
763
- });
764
-
765
- it('disables content-type negotiation when negotiateContentType=false',
766
- function(done) {
767
- givenErrorHandlerForError(new Error('Some error'), {
768
- negotiateContentType: false,
769
- defaultType: 'application/json',
770
- });
771
- request.get('/')
772
- .set('Accept', 'text/html')
773
- .expect('Content-Type', /^application\/json/, done);
774
- });
775
-
776
- it('chooses resolved type when negotiateContentType=false + not-supported',
777
- function(done) {
778
- givenErrorHandlerForError(new Error('Some error'), {
779
- negotiateContentType: false,
780
- defaultType: 'unsupported/type',
781
- });
782
- request.get('/')
783
- .set('Accept', 'text/html')
784
- .expect('Content-Type', /^text\/html/, done);
785
- });
786
-
787
- it('chooses default type when negotiateContentType=false + not-supported ',
788
- function(done) {
789
- givenErrorHandlerForError(new Error('Some error'), {
790
- negotiateContentType: false,
791
- defaultType: 'unsupported/type',
792
- });
793
- request.get('/')
794
- .expect('Content-Type', /^application\/json/, done);
795
- });
796
-
797
- it('honors order of accepted content-types of text/html', function(done) {
798
- givenErrorHandlerForError(new Error('Some error'), {
799
- defaultType: 'application/json',
800
- });
801
- request.get('/')
802
- // text/html will be used because its provided first
803
- .set('Accept', 'text/html, application/json')
804
- .expect('Content-Type', /^text\/html/, done);
805
- });
806
-
807
- it('picks first supported type upon multiple accepted', function(done) {
808
- givenErrorHandlerForError(new Error('Some error'), {
809
- defaultType: 'application/json',
810
- });
811
- request.get('/')
812
- .set('Accept', '*/*, not-supported, text/html, application/json')
813
- .expect('Content-Type', /^text\/html/, done);
814
- });
815
-
816
- it('falls back for unsupported option.defaultType', function(done) {
817
- givenErrorHandlerForError(new Error('Some error'), {
818
- defaultType: 'unsupported',
819
- });
820
- request.get('/')
821
- .set('Accept', '*/*')
822
- .expect('Content-Type', /^application\/json/, done);
823
- });
824
-
825
- it('returns defaultType for unsupported type', function(done) {
826
- givenErrorHandlerForError(new Error('Some error'), {
827
- defaultType: 'text/html',
828
- });
829
- request.get('/')
830
- .set('Accept', 'unsupported/type')
831
- .expect('Content-Type', /^text\/html/, done);
832
- });
833
-
834
- it('supports query _format', function(done) {
835
- givenErrorHandlerForError(new Error('Some error'), {
836
- defaultType: 'text/html',
837
- });
838
- request.get('/?_format=html')
839
- .set('Accept', 'application/json')
840
- .expect('Content-Type', /^text\/html/, done);
841
- });
842
-
843
- it('handles unknown _format query', function() {
844
- givenErrorHandlerForError();
845
- return request.get('/?_format=unknown')
846
- .expect('X-Warning', /_format.*not supported/);
847
- });
848
- });
849
-
850
- it('does not modify "options" argument', function(done) {
851
- const options = {log: false, debug: false};
852
- givenErrorHandlerForError(new Error(), options);
853
- request.get('/').end(function(err) {
854
- if (err) return done(err);
855
- expect(options).to.eql({log: false, debug: false});
856
- done();
857
- });
858
- });
859
- });
860
-
861
- let app, _requestHandler, request, server;
862
- function resetRequestHandler() {
863
- _requestHandler = null;
864
- }
865
-
866
- function givenErrorHandlerForError(error, options) {
867
- if (!error) error = new Error('an error');
868
-
869
- if (!options) options = {};
870
- if (!('log' in options)) {
871
- // Disable logging to console by default, so that we don't spam
872
- // console output. One can use "DEBUG=strong-error-handler" when
873
- // troubleshooting.
874
- options.log = false;
875
- }
876
-
877
- const handler = strongErrorHandler(options);
878
- _requestHandler = function(req, res, next) {
879
- debug('Invoking strong-error-handler');
880
- handler(error, req, res, next);
881
- };
882
- }
883
-
884
- function setupHttpServerAndClient(done) {
885
- app = express();
886
- app.use(function(req, res, next) {
887
- if (!_requestHandler) {
888
- const msg = 'Error handler middleware was not setup in this test';
889
- console.error(msg);
890
- res.statusCode = 500;
891
- res.setHeader('Content-Type', 'text/plain; charset=utf-8');
892
- res.end(msg);
893
- return;
894
- }
895
-
896
- _requestHandler(req, res, warnUnhandledError);
897
-
898
- function warnUnhandledError(err) {
899
- console.log('unexpected: strong-error-handler called next with',
900
- (err && (err.stack || err)) || 'no error');
901
- res.statusCode = 500;
902
- res.setHeader('Content-Type', 'text/plain; charset=utf-8');
903
- res.end(err ?
904
- 'Unhandled strong-error-handler error:\n' + (err.stack || err) :
905
- 'The error was silently discared by strong-error-handler');
906
- }
907
- });
908
-
909
- server = app.listen(0, function() {
910
- const url = 'http://127.0.0.1:' + this.address().port;
911
- debug('Test server listening on %s', url);
912
- request = supertest(app);
913
- done();
914
- })
915
- .once('error', function(err) {
916
- debug('Cannot setup HTTP server: %s', err.stack);
917
- done(err);
918
- });
919
- }
920
-
921
- function stopHttpServerAndClient() {
922
- server.close();
923
- }
924
-
925
- function ErrorWithProps(props) {
926
- this.name = props.name || 'ErrorWithProps';
927
- for (const p in props) {
928
- this[p] = props[p];
929
- }
930
-
931
- if (Error.captureStackTrace) {
932
- // V8 (Chrome, Opera, Node)
933
- Error.captureStackTrace(this, this.constructor);
934
- }
935
- }
936
- util.inherits(ErrorWithProps, Error);
937
-
938
- function getExpectedErrorData(err) {
939
- const data = {};
940
- cloneAllProperties(data, err);
941
- return data;
942
- }