From 1f966a040b4099a6f66546d59edae8c800401a96 Mon Sep 17 00:00:00 2001 From: kwypchlo Date: Fri, 14 Nov 2014 22:04:56 +0100 Subject: [PATCH 1/2] fix(toJson): allow JSON.stringify exceptions to be suppressed Adds third parameter called options to toJson function (for the time being undocumented, private). Adds support for suppressExceptions option (with test cases) that suppresses any exceptions that JSON.stringify might throw and returns undefined in such case. This fix has been discussed on issue #9838 --- src/Angular.js | 14 +++++++++++++- test/AngularSpec.js | 27 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/src/Angular.js b/src/Angular.js index aab712d3e1ee..f502475a4ed3 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -967,8 +967,20 @@ function toJsonReplacer(key, value) { * @param {boolean=} pretty If set to true, the JSON output will contain newlines and whitespace. * @returns {string|undefined} JSON-ified string representing `obj`. */ -function toJson(obj, pretty) { +function toJson(obj, pretty, options) { if (typeof obj === 'undefined') return undefined; + + if (!options) options = {}; + + if (options.suppressExceptions) { + try { + return JSON.stringify(obj, toJsonReplacer, pretty ? ' ' : null); + } catch (e) { + // in case JSON.stringify throws, suppress error and return undefined + return undefined; + } + } + return JSON.stringify(obj, toJsonReplacer, pretty ? ' ' : null); } diff --git a/test/AngularSpec.js b/test/AngularSpec.js index 5ab87771eb58..922e672b7081 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -1219,6 +1219,33 @@ describe('angular', function() { it('should serialize undefined as undefined', function() { expect(toJson(undefined)).toEqual(undefined); }); + + it('should allow JSON.stringify to throw exceptions by default', function() { + // create objects with references to each other (circular reference) + // passing such object to JSON.stringify throws a TypeError + var a = {}, b = {test: a}; + a.test = b; + + // prepare toJson function with correct arguments for this test + var toJsonSuppressedTest = toJson.bind(undefined, a); + + // it should throw a TypeError + expect(toJsonSuppressedTest).toThrow(); + }); + + it('should not throw exceptions if suppressExceptions option is passed', function() { + // create objects with references to each other (circular reference) + // passing such object to JSON.stringify throws a TypeError + var a = {}, b = {test: a}; + a.test = b; + + // prepare toJson function with correct arguments for this test + var toJsonSuppressedTest = toJson.bind(undefined, a, false, {suppressExceptions: true}); + + // it shouldn't throw a TypeError and it should return undefined + expect(toJsonSuppressedTest).not.toThrow(); + expect(toJsonSuppressedTest()).toBeUndefined(); + }); }); describe('isElement', function() { From 365630e80876d7159c31fdd53b7114800a152d9a Mon Sep 17 00:00:00 2001 From: kwypchlo Date: Fri, 14 Nov 2014 22:54:04 +0100 Subject: [PATCH 2/2] fix(toJson): suppress toJson exceptions in core functions that prepare error messages for user Makes use of suppressExceptions option parameter in toJson function. Closes #9838 --- src/minErr.js | 2 +- src/ng/directive/ngRepeat.js | 2 +- src/ng/rootScope.js | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/minErr.js b/src/minErr.js index 96171915b7d7..d89bfde0cc18 100644 --- a/src/minErr.js +++ b/src/minErr.js @@ -59,7 +59,7 @@ function minErr(module, ErrorConstructor) { } else if (typeof arg === 'undefined') { return 'undefined'; } else if (typeof arg !== 'string') { - return toJson(arg); + return toJson(arg, false, {suppressExceptions: true}); } return arg; } diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js index f0c2660faa5c..b97bc7604b29 100644 --- a/src/ng/directive/ngRepeat.js +++ b/src/ng/directive/ngRepeat.js @@ -366,7 +366,7 @@ var ngRepeatDirective = ['$parse', '$animate', function($parse, $animate) { }); throw ngRepeatMinErr('dupes', "Duplicates in a repeater are not allowed. Use 'track by' expression to specify unique keys. Repeater: {0}, Duplicate key: {1}, Duplicate value: {2}", - expression, trackById, toJson(value)); + expression, trackById, toJson(value, false, {suppressExceptions: true})); } else { // new never before seen block nextBlockOrder[index] = {id: trackById, scope: undefined, clone: undefined}; diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js index df8ddcf6d4cc..28957b4bd134 100644 --- a/src/ng/rootScope.js +++ b/src/ng/rootScope.js @@ -776,7 +776,8 @@ function $RootScopeProvider() { logMsg = (isFunction(watch.exp)) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp; - logMsg += '; newVal: ' + toJson(value) + '; oldVal: ' + toJson(last); + logMsg += '; newVal: ' + toJson(value, false, {suppressExceptions: true}) + + '; oldVal: ' + toJson(last, false, {suppressExceptions: true}); watchLog[logIdx].push(logMsg); } } else if (watch === lastDirtyWatch) { @@ -810,7 +811,7 @@ function $RootScopeProvider() { throw $rootScopeMinErr('infdig', '{0} $digest() iterations reached. Aborting!\n' + 'Watchers fired in the last 5 iterations: {1}', - TTL, toJson(watchLog)); + TTL, toJson(watchLog, false, {suppressExceptions: true})); } } while (dirty || asyncQueue.length);