From 6e759874d7de879cea6aff1773098de6d899878f Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Sun, 20 Jun 2021 10:41:20 -0400 Subject: [PATCH 01/11] failing testcase --- spec/CloudCode.spec.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index c53a284273..66ca095a16 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -2519,6 +2519,38 @@ describe('afterFind hooks', () => { }); }); + it('should expose context in beforeSave/afterSave via header', async () => { + let calledBefore = false; + let calledAfter = false; + Parse.Cloud.beforeSave('TestObject', req => { + expect(req.object.get('foo')).toEqual('bar'); + expect(req.context.otherKey).toBe(1); + expect(req.context.key).toBe('value'); + calledBefore = true; + }); + Parse.Cloud.afterSave('TestObject', req => { + expect(req.object.get('foo')).toEqual('bar'); + expect(req.context.otherKey).toBe(1); + expect(req.context.key).toBe('value'); + calledAfter = true; + }); + const req = request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/TestObject', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Context': '{"key":"value","otherKey":1}', + }, + body: { + foo: 'bar', + }, + }); + const result = await req; + expect(calledBefore).toBe(true); + expect(calledAfter).toBe(true); + }); + it('should expose context in before and afterSave', async () => { let calledBefore = false; let calledAfter = false; From 0c23bfd4c1c7ddfb50ae0ef7f154b36cd0b7a638 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Sun, 20 Jun 2021 10:56:07 -0400 Subject: [PATCH 02/11] add header --- CHANGELOG.md | 1 + spec/CloudCode.spec.js | 2 +- src/middlewares.js | 3 +-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fb0b32bac..bdb6b736d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -134,6 +134,7 @@ ___ - Add NPM package-lock version check to CI (Manuel Trezza) [#7333](https://github.com/parse-community/parse-server/pull/7333) - Fix incorrect LiveQuery events triggered for multiple subscriptions on the same class with different events [#7341](https://github.com/parse-community/parse-server/pull/7341) - Fix select and excludeKey queries to properly accept JSON string arrays. Also allow nested fields in exclude (Corey Baker) [#7242](https://github.com/parse-community/parse-server/pull/7242) +- Add context header X-Parse-Context (Corey Baker) [#7437](https://github.com/parse-community/parse-server/pull/7437) ___ ## 4.5.0 diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 66ca095a16..dc8adaec72 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -2546,7 +2546,7 @@ describe('afterFind hooks', () => { foo: 'bar', }, }); - const result = await req; + await req; expect(calledBefore).toBe(true); expect(calledAfter).toBe(true); }); diff --git a/src/middlewares.js b/src/middlewares.js index 1c0a372031..26cef95383 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -24,7 +24,6 @@ const getMountForRequest = function (req) { // req.auth - the Auth for this request export function handleParseHeaders(req, res, next) { var mount = getMountForRequest(req); - var info = { appId: req.get('X-Parse-Application-Id'), sessionToken: req.get('X-Parse-Session-Token'), @@ -35,7 +34,7 @@ export function handleParseHeaders(req, res, next) { dotNetKey: req.get('X-Parse-Windows-Key'), restAPIKey: req.get('X-Parse-REST-API-Key'), clientVersion: req.get('X-Parse-Client-Version'), - context: {}, + context: req.get('X-Parse-Context') == null ? {} : JSON.parse(req.get('X-Parse-Context')), }; var basicAuth = httpAuth(req); From 9e24f488f182ea075906f36580907eb480be8905 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Sun, 20 Jun 2021 13:23:52 -0400 Subject: [PATCH 03/11] switch to X-Parse-Cloud-Context header --- CHANGELOG.md | 2 +- spec/CloudCode.spec.js | 2 +- src/middlewares.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdb6b736d2..929344ab5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -134,7 +134,7 @@ ___ - Add NPM package-lock version check to CI (Manuel Trezza) [#7333](https://github.com/parse-community/parse-server/pull/7333) - Fix incorrect LiveQuery events triggered for multiple subscriptions on the same class with different events [#7341](https://github.com/parse-community/parse-server/pull/7341) - Fix select and excludeKey queries to properly accept JSON string arrays. Also allow nested fields in exclude (Corey Baker) [#7242](https://github.com/parse-community/parse-server/pull/7242) -- Add context header X-Parse-Context (Corey Baker) [#7437](https://github.com/parse-community/parse-server/pull/7437) +- Add context header X-Parse-Cloud-Context (Corey Baker) [#7437](https://github.com/parse-community/parse-server/pull/7437) ___ ## 4.5.0 diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index dc8adaec72..6379d2721d 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -2540,7 +2540,7 @@ describe('afterFind hooks', () => { headers: { 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest', - 'X-Parse-Context': '{"key":"value","otherKey":1}', + 'X-Parse-Cloud-Context': '{"key":"value","otherKey":1}', }, body: { foo: 'bar', diff --git a/src/middlewares.js b/src/middlewares.js index 26cef95383..c066b2f11a 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -34,7 +34,7 @@ export function handleParseHeaders(req, res, next) { dotNetKey: req.get('X-Parse-Windows-Key'), restAPIKey: req.get('X-Parse-REST-API-Key'), clientVersion: req.get('X-Parse-Client-Version'), - context: req.get('X-Parse-Context') == null ? {} : JSON.parse(req.get('X-Parse-Context')), + context: req.get('X-Parse-Cloud-Context') == null ? {} : JSON.parse(req.get('X-Parse-Cloud-Context')), }; var basicAuth = httpAuth(req); From a1be895cc55519586d2dab9f25a0b7616c3bb54c Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Sun, 20 Jun 2021 19:01:59 -0400 Subject: [PATCH 04/11] add back blank line that lint removed --- src/middlewares.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/middlewares.js b/src/middlewares.js index c066b2f11a..e4c5ee2dd5 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -24,6 +24,7 @@ const getMountForRequest = function (req) { // req.auth - the Auth for this request export function handleParseHeaders(req, res, next) { var mount = getMountForRequest(req); + var info = { appId: req.get('X-Parse-Application-Id'), sessionToken: req.get('X-Parse-Session-Token'), From 31589161b70a34aa2810b3f0b96a0780e8633a10 Mon Sep 17 00:00:00 2001 From: Corey Date: Mon, 21 Jun 2021 16:08:01 -0400 Subject: [PATCH 05/11] test replacing context header with body context. Add support for setting body with json string --- spec/CloudCode.spec.js | 126 +++++++++++++++++++++++++++++++++++++++++ src/middlewares.js | 28 +++++++-- 2 files changed, 150 insertions(+), 4 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 6379d2721d..e6815a54d7 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -2519,6 +2519,38 @@ describe('afterFind hooks', () => { }); }); + it('should throw error if context header is malformed', async () => { + let calledBefore = false; + let calledAfter = false; + Parse.Cloud.beforeSave('TestObject', () => { + calledBefore = true; + }); + Parse.Cloud.afterSave('TestObject', () => { + calledAfter = true; + }); + const req = request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/TestObject', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Cloud-Context': 'key', + }, + body: { + foo: 'bar', + }, + }); + try { + await req; + fail('Should have thrown error'); + } catch (e) { + expect(e).toBeDefined(); + expect(e.data.code).toEqual(Parse.Error.INVALID_JSON); + } + expect(calledBefore).toBe(false); + expect(calledAfter).toBe(false); + }); + it('should expose context in beforeSave/afterSave via header', async () => { let calledBefore = false; let calledAfter = false; @@ -2551,6 +2583,39 @@ describe('afterFind hooks', () => { expect(calledAfter).toBe(true); }); + it('should override header context with body context in beforeSave/afterSave', async () => { + let calledBefore = false; + let calledAfter = false; + Parse.Cloud.beforeSave('TestObject', req => { + expect(req.object.get('foo')).toEqual('bar'); + expect(req.context.otherKey).toBe(10); + expect(req.context.key).toBe('hello'); + calledBefore = true; + }); + Parse.Cloud.afterSave('TestObject', req => { + expect(req.object.get('foo')).toEqual('bar'); + expect(req.context.otherKey).toBe(10); + expect(req.context.key).toBe('hello'); + calledAfter = true; + }); + const req = request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/TestObject', + headers: { + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Cloud-Context': '{"key":"value","otherKey":1}', + }, + body: { + foo: 'bar', + _ApplicationId: 'test', + _context: '{"key":"hello","otherKey":10}', + }, + }); + await req; + expect(calledBefore).toBe(true); + expect(calledAfter).toBe(true); + }); + it('should expose context in before and afterSave', async () => { let calledBefore = false; let calledAfter = false; @@ -2836,6 +2901,67 @@ describe('afterLogin hook', () => { done(); }); + it('context options should override _context object property when saving a new object', async () => { + Parse.Cloud.beforeSave('TestObject', req => { + expect(req.context.a).toEqual('a'); + expect(req.context.hello).not.toBeDefined(); + expect(req._context).not.toBeDefined(); + expect(req.object._context).not.toBeDefined(); + expect(req.object.context).not.toBeDefined(); + }); + Parse.Cloud.afterSave('TestObject', req => { + expect(req.context.a).toEqual('a'); + expect(req.context.hello).not.toBeDefined(); + expect(req._context).not.toBeDefined(); + expect(req.object._context).not.toBeDefined(); + expect(req.object.context).not.toBeDefined(); + }); + const obj = new TestObject(); + obj.set('_context', { hello: 'world' }); + await obj.save(null, { context: { a: 'a' } }); + }); + + xit('should throw error if _context option is malformed', async () => { + let calledBefore = false; + let calledAfter = false; + Parse.Cloud.beforeSave('TestObject', () => { + calledBefore = true; + }); + Parse.Cloud.afterSave('TestObject', () => { + calledAfter = true; + }); + const obj = new TestObject(); + try { + await obj.save(null, { context: "{ a: 'a' }" }); + fail('Should have thrown error'); + } catch (e) { + expect(e).toBeDefined(); + } + expect(calledBefore).toBe(false); + expect(calledAfter).toBe(false); + }); + + xit('should throw error if _context body is malformed', async () => { + let calledBefore = false; + let calledAfter = false; + Parse.Cloud.beforeSave('TestObject', () => { + calledBefore = true; + }); + Parse.Cloud.afterSave('TestObject', () => { + calledAfter = true; + }); + const obj = new TestObject(); + try { + obj.set('_context', "{ a: 'a' }"); + await obj.save(); + fail('Should have thrown error'); + } catch (e) { + expect(e).toBeDefined(); + } + expect(calledBefore).toBe(false); + expect(calledAfter).toBe(false); + }); + it('should have access to context when saving a new object', async () => { Parse.Cloud.beforeSave('TestObject', req => { expect(req.context.a).toEqual('a'); diff --git a/src/middlewares.js b/src/middlewares.js index e4c5ee2dd5..f1fd0f6a8d 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -24,7 +24,14 @@ const getMountForRequest = function (req) { // req.auth - the Auth for this request export function handleParseHeaders(req, res, next) { var mount = getMountForRequest(req); - + let context = {}; + if (req.get('X-Parse-Cloud-Context') != null) { + try { + context = JSON.parse(req.get('X-Parse-Cloud-Context')); + } catch (e) { + return malformedContext(req, res); + } + } var info = { appId: req.get('X-Parse-Application-Id'), sessionToken: req.get('X-Parse-Session-Token'), @@ -35,7 +42,7 @@ export function handleParseHeaders(req, res, next) { dotNetKey: req.get('X-Parse-Windows-Key'), restAPIKey: req.get('X-Parse-REST-API-Key'), clientVersion: req.get('X-Parse-Client-Version'), - context: req.get('X-Parse-Cloud-Context') == null ? {} : JSON.parse(req.get('X-Parse-Cloud-Context')), + context: context, }; var basicAuth = httpAuth(req); @@ -105,8 +112,16 @@ export function handleParseHeaders(req, res, next) { info.masterKey = req.body._MasterKey; delete req.body._MasterKey; } - if (req.body._context && req.body._context instanceof Object) { - info.context = req.body._context; + if (req.body._context) { + if (req.body._context instanceof Object) { + info.context = req.body._context; + } else { + try { + info.context = JSON.parse(req.body._context); + } catch (e) { + return malformedContext(req, res); + } + } delete req.body._context; } if (req.body._ContentType) { @@ -454,3 +469,8 @@ function invalidRequest(req, res) { res.status(403); res.end('{"error":"unauthorized"}'); } + +function malformedContext(req, res) { + res.status(500); + res.json({ code: Parse.Error.INVALID_JSON, error: 'Invalid object for context.' }); +} From 0fa1d3a3e9bf1e2e52693ae725bc2bfacaf97e33 Mon Sep 17 00:00:00 2001 From: Corey Date: Mon, 21 Jun 2021 16:18:55 -0400 Subject: [PATCH 06/11] add back blank line --- src/middlewares.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/middlewares.js b/src/middlewares.js index f1fd0f6a8d..cd24deb19b 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -24,6 +24,7 @@ const getMountForRequest = function (req) { // req.auth - the Auth for this request export function handleParseHeaders(req, res, next) { var mount = getMountForRequest(req); + let context = {}; if (req.get('X-Parse-Cloud-Context') != null) { try { From bb972b8a40a85f2a258cb99132c9c2d1d15f208d Mon Sep 17 00:00:00 2001 From: Corey Date: Mon, 21 Jun 2021 16:28:25 -0400 Subject: [PATCH 07/11] cover error when _context body is wrong --- spec/CloudCode.spec.js | 74 +++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 41 deletions(-) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index e6815a54d7..3da82b5b17 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -2616,6 +2616,39 @@ describe('afterFind hooks', () => { expect(calledAfter).toBe(true); }); + it('should throw error if context body is malformed', async () => { + let calledBefore = false; + let calledAfter = false; + Parse.Cloud.beforeSave('TestObject', () => { + calledBefore = true; + }); + Parse.Cloud.afterSave('TestObject', () => { + calledAfter = true; + }); + const req = request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/TestObject', + headers: { + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Cloud-Context': '{"key":"value","otherKey":1}', + }, + body: { + foo: 'bar', + _ApplicationId: 'test', + _context: 'key', + }, + }); + try { + await req; + fail('Should have thrown error'); + } catch (e) { + expect(e).toBeDefined(); + expect(e.data.code).toEqual(Parse.Error.INVALID_JSON); + } + expect(calledBefore).toBe(false); + expect(calledAfter).toBe(false); + }); + it('should expose context in before and afterSave', async () => { let calledBefore = false; let calledAfter = false; @@ -2921,47 +2954,6 @@ describe('afterLogin hook', () => { await obj.save(null, { context: { a: 'a' } }); }); - xit('should throw error if _context option is malformed', async () => { - let calledBefore = false; - let calledAfter = false; - Parse.Cloud.beforeSave('TestObject', () => { - calledBefore = true; - }); - Parse.Cloud.afterSave('TestObject', () => { - calledAfter = true; - }); - const obj = new TestObject(); - try { - await obj.save(null, { context: "{ a: 'a' }" }); - fail('Should have thrown error'); - } catch (e) { - expect(e).toBeDefined(); - } - expect(calledBefore).toBe(false); - expect(calledAfter).toBe(false); - }); - - xit('should throw error if _context body is malformed', async () => { - let calledBefore = false; - let calledAfter = false; - Parse.Cloud.beforeSave('TestObject', () => { - calledBefore = true; - }); - Parse.Cloud.afterSave('TestObject', () => { - calledAfter = true; - }); - const obj = new TestObject(); - try { - obj.set('_context', "{ a: 'a' }"); - await obj.save(); - fail('Should have thrown error'); - } catch (e) { - expect(e).toBeDefined(); - } - expect(calledBefore).toBe(false); - expect(calledAfter).toBe(false); - }); - it('should have access to context when saving a new object', async () => { Parse.Cloud.beforeSave('TestObject', req => { expect(req.context.a).toEqual('a'); From a5519573dbad4ca8fc7ff8a899a555cf6b5de075 Mon Sep 17 00:00:00 2001 From: Corey Date: Mon, 21 Jun 2021 17:03:35 -0400 Subject: [PATCH 08/11] Update middlewares.js --- src/middlewares.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/middlewares.js b/src/middlewares.js index cd24deb19b..a638169b27 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -467,11 +467,11 @@ export function promiseEnsureIdempotency(req) { } function invalidRequest(req, res) { - res.status(403); + res.status(400); res.end('{"error":"unauthorized"}'); } function malformedContext(req, res) { - res.status(500); + res.status(400); res.json({ code: Parse.Error.INVALID_JSON, error: 'Invalid object for context.' }); } From 8503ce64083f723c01464847514d7686e0fb19c3 Mon Sep 17 00:00:00 2001 From: Corey Date: Mon, 21 Jun 2021 17:36:33 -0400 Subject: [PATCH 09/11] revert accidental status change --- src/middlewares.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middlewares.js b/src/middlewares.js index a638169b27..10c1e87354 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -467,7 +467,7 @@ export function promiseEnsureIdempotency(req) { } function invalidRequest(req, res) { - res.status(400); + res.status(403); res.end('{"error":"unauthorized"}'); } From 3d4a4dd3da6a6474e5ed00b4a3c209d9174d12fe Mon Sep 17 00:00:00 2001 From: Corey Date: Mon, 21 Jun 2021 18:43:46 -0400 Subject: [PATCH 10/11] make sure context always decodes to an object else throw error --- spec/CloudCode.spec.js | 65 ++++++++++++++++++++++++++++++++++++++++++ src/middlewares.js | 6 ++++ 2 files changed, 71 insertions(+) diff --git a/spec/CloudCode.spec.js b/spec/CloudCode.spec.js index 3da82b5b17..86a7627427 100644 --- a/spec/CloudCode.spec.js +++ b/spec/CloudCode.spec.js @@ -2551,6 +2551,38 @@ describe('afterFind hooks', () => { expect(calledAfter).toBe(false); }); + it('should throw error if context header is string "1"', async () => { + let calledBefore = false; + let calledAfter = false; + Parse.Cloud.beforeSave('TestObject', () => { + calledBefore = true; + }); + Parse.Cloud.afterSave('TestObject', () => { + calledAfter = true; + }); + const req = request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/TestObject', + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Cloud-Context': '1', + }, + body: { + foo: 'bar', + }, + }); + try { + await req; + fail('Should have thrown error'); + } catch (e) { + expect(e).toBeDefined(); + expect(e.data.code).toEqual(Parse.Error.INVALID_JSON); + } + expect(calledBefore).toBe(false); + expect(calledAfter).toBe(false); + }); + it('should expose context in beforeSave/afterSave via header', async () => { let calledBefore = false; let calledAfter = false; @@ -2649,6 +2681,39 @@ describe('afterFind hooks', () => { expect(calledAfter).toBe(false); }); + it('should throw error if context body is string "true"', async () => { + let calledBefore = false; + let calledAfter = false; + Parse.Cloud.beforeSave('TestObject', () => { + calledBefore = true; + }); + Parse.Cloud.afterSave('TestObject', () => { + calledAfter = true; + }); + const req = request({ + method: 'POST', + url: 'http://localhost:8378/1/classes/TestObject', + headers: { + 'X-Parse-REST-API-Key': 'rest', + 'X-Parse-Cloud-Context': '{"key":"value","otherKey":1}', + }, + body: { + foo: 'bar', + _ApplicationId: 'test', + _context: 'true', + }, + }); + try { + await req; + fail('Should have thrown error'); + } catch (e) { + expect(e).toBeDefined(); + expect(e.data.code).toEqual(Parse.Error.INVALID_JSON); + } + expect(calledBefore).toBe(false); + expect(calledAfter).toBe(false); + }); + it('should expose context in before and afterSave', async () => { let calledBefore = false; let calledAfter = false; diff --git a/src/middlewares.js b/src/middlewares.js index 10c1e87354..92c9e1d298 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -29,6 +29,9 @@ export function handleParseHeaders(req, res, next) { if (req.get('X-Parse-Cloud-Context') != null) { try { context = JSON.parse(req.get('X-Parse-Cloud-Context')); + if (typeof context !== 'object') { + throw 'Context is not an object'; + } } catch (e) { return malformedContext(req, res); } @@ -119,6 +122,9 @@ export function handleParseHeaders(req, res, next) { } else { try { info.context = JSON.parse(req.body._context); + if (typeof info.context !== 'object') { + throw 'Context is not an object'; + } } catch (e) { return malformedContext(req, res); } From 33087a05f74bcb3b8fecc41b012a64aa81fb4296 Mon Sep 17 00:00:00 2001 From: Corey Date: Mon, 21 Jun 2021 20:13:49 -0400 Subject: [PATCH 11/11] improve context object check --- src/middlewares.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/middlewares.js b/src/middlewares.js index 92c9e1d298..88de107264 100644 --- a/src/middlewares.js +++ b/src/middlewares.js @@ -29,7 +29,7 @@ export function handleParseHeaders(req, res, next) { if (req.get('X-Parse-Cloud-Context') != null) { try { context = JSON.parse(req.get('X-Parse-Cloud-Context')); - if (typeof context !== 'object') { + if (Object.prototype.toString.call(context) !== '[object Object]') { throw 'Context is not an object'; } } catch (e) { @@ -122,7 +122,7 @@ export function handleParseHeaders(req, res, next) { } else { try { info.context = JSON.parse(req.body._context); - if (typeof info.context !== 'object') { + if (Object.prototype.toString.call(info.context) !== '[object Object]') { throw 'Context is not an object'; } } catch (e) {