Skip to content

Commit 5e8cc5b

Browse files
author
Adam Crabtree
authored
Gracefully handle formatters throwing restify errors. (#1807)
Avoid uncaughtExceptions for expected & recoverable failures scenarios within formatters. Add to JSON formatter failed serialization recovery.
1 parent c709aae commit 5e8cc5b

4 files changed

Lines changed: 114 additions & 5 deletions

File tree

lib/formatters/json.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
'use strict';
44

5+
var errors = require('restify-errors');
6+
57
///--- Exports
68

79
/**
@@ -16,7 +18,18 @@
1618
* @returns {String} data
1719
*/
1820
function formatJSON(req, res, body) {
19-
var data = body ? JSON.stringify(body) : 'null';
21+
var data = 'null';
22+
if (body) {
23+
try {
24+
data = JSON.stringify(body);
25+
} catch (e) {
26+
throw new errors.InternalServerError(
27+
{ cause: e, info: { formatter: 'json' } },
28+
'could not format response body'
29+
);
30+
}
31+
}
32+
2033
// Setting the content-length header is not a formatting feature and should
2134
// be separated into another module
2235
res.setHeader('Content-Length', Buffer.byteLength(data));

lib/response.js

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,8 @@ function patch(Response) {
494494
);
495495
}
496496

497+
var formatterType = type;
498+
497499
if (self._charSet) {
498500
type = type + '; charset=' + self._charSet;
499501
}
@@ -506,7 +508,26 @@ function patch(Response) {
506508
// Finally, invoke the formatter and flush the request with it's
507509
// results
508510

509-
return flush(self, formatter(self.req, self, body));
511+
var formattedBody;
512+
try {
513+
formattedBody = formatter(self.req, self, body);
514+
} catch (e) {
515+
if (
516+
e instanceof errors.RestError ||
517+
e instanceof errors.HttpError
518+
) {
519+
var res = formatterError(
520+
self,
521+
e,
522+
'error in formatter (' +
523+
formatterType +
524+
') formatting response body'
525+
);
526+
return res;
527+
}
528+
throw e;
529+
}
530+
return flush(self, formattedBody);
510531
}
511532
}
512533

@@ -873,22 +894,27 @@ function flush(res, body) {
873894
* @function formatterError
874895
* @param {Response} res - response
875896
* @param {Error} err - error
897+
* @param {String} [msg] - custom log message
876898
* @returns {Response} response
877899
*/
878-
function formatterError(res, err) {
900+
function formatterError(res, err, msg) {
879901
// If the user provided a non-success error code, we don't want to
880902
// mess with it since their error is probably more important than
881903
// our inability to format their message.
882904
if (res.statusCode >= 200 && res.statusCode < 300) {
883905
res.statusCode = err.statusCode;
884906
}
885907

908+
if (typeof msg !== 'string') {
909+
msg = 'error retrieving formatter';
910+
}
911+
886912
res.log.warn(
887913
{
888914
req: res.req,
889915
err: err
890916
},
891-
'error retrieving formatter'
917+
msg
892918
);
893919

894920
return flush(res);

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@
141141
"proxyquire": "^1.8.0",
142142
"restify-clients": "^2.6.6",
143143
"rimraf": "^2.6.3",
144+
"sinon": "^7.5.0",
144145
"validator": "^7.2.0",
145146
"watershed": "^0.4.0"
146147
},

test/formatter.test.js

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44
/* eslint-disable func-names */
55

66
var restifyClients = require('restify-clients');
7+
var errors = require('restify-errors');
8+
var sinon = require('sinon');
79

810
var restify = require('../lib');
11+
var jsonFormatter = require('../lib/formatters/json');
912

1013
if (require.cache[__dirname + '/lib/helper.js']) {
1114
delete require.cache[__dirname + '/lib/helper.js'];
@@ -36,6 +39,9 @@ before(function(callback) {
3639
// this is a bad formatter, on purpose.
3740
return x.toString(); // eslint-disable-line no-undef
3841
},
42+
'text/syncerror_expected': function(req, res, body) {
43+
throw new errors.InternalServerError('Errors happen');
44+
},
3945
'application/foo; q=0.9': function(req, res, body) {
4046
return 'foo!';
4147
},
@@ -108,7 +114,7 @@ test('GH-845: sync formatter', function(t) {
108114
});
109115

110116
test('GH-845: sync formatter should blow up', function(t) {
111-
SERVER.on('uncaughtException', function(req, res, route, err) {
117+
SERVER.once('uncaughtException', function(req, res, route, err) {
112118
t.ok(err);
113119
t.equal(err.name, 'ReferenceError');
114120
t.equal(err.message, 'x is not defined');
@@ -130,6 +136,29 @@ test('GH-845: sync formatter should blow up', function(t) {
130136
);
131137
});
132138

139+
test('sync formatter should handle expected errors gracefully', function(t) {
140+
SERVER.once('uncaughtException', function(req, res, route, err) {
141+
throw new Error('Should not reach');
142+
});
143+
144+
CLIENT.get(
145+
{
146+
path: '/sync',
147+
headers: {
148+
accept: 'text/syncerror_expected'
149+
}
150+
},
151+
function(err, req, res, data) {
152+
t.ok(err);
153+
t.ok(req);
154+
t.ok(res);
155+
t.equal(res.statusCode, 500);
156+
SERVER.removeAllListeners('uncaughtException');
157+
t.end();
158+
}
159+
);
160+
});
161+
133162
test('q-val priority', function(t) {
134163
var opts = {
135164
path: '/sync',
@@ -228,3 +257,43 @@ test('default jsonp formatter should escape line and paragraph separators', func
228257
t.end();
229258
});
230259
});
260+
261+
// eslint-disable-next-line
262+
test('default json formatter should wrap & throw InternalServer error on unserializable bodies', function(t) {
263+
t.expect(2);
264+
265+
sinon.spy(JSON, 'stringify');
266+
267+
SERVER.once('uncaughtException', function(req, res, route, err) {
268+
console.log(err.stack); // For convenience
269+
throw new Error('Should not reach');
270+
});
271+
272+
var opts = {
273+
path: '/badJSON',
274+
name: 'badJSON'
275+
};
276+
277+
SERVER.get(opts, function(req, res, next) {
278+
var body = {};
279+
// Add unserializable circular reference
280+
body.body = body;
281+
282+
try {
283+
jsonFormatter(req, res, body);
284+
throw new Error('Should not reach');
285+
} catch (e) {
286+
t.ok(e instanceof errors.InternalServerError);
287+
t.ok(JSON.stringify.threw(e.cause()));
288+
}
289+
290+
res.send();
291+
});
292+
293+
CLIENT.get('/badJSON', function(err, req, res, data) {
294+
SERVER.rm('badJSON');
295+
SERVER.removeAllListeners('uncaughtException');
296+
JSON.stringify.restore();
297+
t.end();
298+
});
299+
});

0 commit comments

Comments
 (0)