Skip to content

Commit

Permalink
Merge pull request #108 from DougMidgley/feature/106-weird-login-fail…
Browse files Browse the repository at this point in the history
…ed-soap-error-upon-retrieving-folders

Standardized Error Messages returned where possible
  • Loading branch information
JoernBerkefeld committed May 11, 2022
2 parents 5a0fea8 + 9a7ff7c commit d2c1a68
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 99 deletions.
7 changes: 4 additions & 3 deletions lib/auth.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

const axios = require('axios');
const { isConnectionError } = require('./util');
const { isConnectionError, RestError } = require('./util');
const AVAIALABLE_SCOPES = [
'accounts_read',
'accounts_write',
Expand Down Expand Up @@ -166,9 +166,9 @@ module.exports = class Auth {
this.options.eventHandlers.onConnectionError(ex, remainingAttempts);
}
return this.getAccessToken(forceRefresh, remainingAttempts);
} else {
throw ex;
}

throw new RestError(ex);
}

if (this.options?.eventHandlers?.onRefresh) {
Expand All @@ -180,6 +180,7 @@ module.exports = class Auth {

/**
* Helper to get back list of scopes supported by SDK
*
* @returns {Array[String]} array of potential scopes
*/
getSupportedScopes() {
Expand Down
4 changes: 2 additions & 2 deletions lib/rest.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

const axios = require('axios');
const { isObject, isPayload, isConnectionError } = require('./util');
const { isObject, isPayload, isConnectionError, RestError } = require('./util');
const pLimit = require('p-limit');

module.exports = class Rest {
Expand Down Expand Up @@ -203,7 +203,7 @@ module.exports = class Rest {
//only retry once on refresh since there should be no reason for this token to be invalid
return this._apiRequest(requestOptions, 1);
} else {
throw ex;
throw new RestError(ex);
}
}
}
Expand Down
44 changes: 12 additions & 32 deletions lib/soap.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
'use strict';
const axios = require('axios');
const { XMLBuilder, XMLParser } = require('fast-xml-parser');
const { isObject, isConnectionError } = require('./util');

const { isObject, isConnectionError, SOAPError } = require('./util');

module.exports = class Soap {
/**
Expand Down Expand Up @@ -385,7 +386,7 @@ module.exports = class Soap {
response = ex.response;
} else {
// if no response, then throw
throw ex;
throw new SOAPError(ex);
}
}
if (this.options?.eventHandlers?.logResponse) {
Expand All @@ -402,13 +403,17 @@ module.exports = class Soap {
// need to wait as it may error
return await _parseResponse(response, options.key);
} catch (ex) {
if (ex.errorMessage === 'Token Expired' && remainingAttempts) {
if (ex.message === 'Token Expired' && remainingAttempts) {
// force refresh due to url related issue
await this.auth.getAccessToken(true);
// set to no more retries as after token refresh it should always work
return this._apiRequest(options, 1);
} else {
} else if (ex instanceof SOAPError) {
//rethrow as is already handled/parsed
throw ex;
} else {
//unknown error
throw new SOAPError(ex, response, null);
}
}
}
Expand Down Expand Up @@ -486,11 +491,12 @@ async function _parseResponse(response, key) {
}
// checks overall status error
if (['Error', 'Has Errors'].includes(soapBody[key].OverallStatus)) {
throw new SOAPError(response, soapBody[key]);
throw new SOAPError(null, response, soapBody[key]);
}
return soapBody[key];
}
throw new SOAPError(response, soapBody);
// something else went wrong but payload parsed
throw new SOAPError(null, response, soapBody);
}
/**
* Method checks options object for validity
Expand All @@ -517,29 +523,3 @@ function validateOptions(options, additional) {
}
}
}

class SOAPError extends Error {
constructor(response, soapBody) {
// Content Error
if (soapBody && ['Error', 'Has Errors'].includes(soapBody.OverallStatus)) {
super('One or more errors in the Results');
}
// Payload Error
else if (soapBody && soapBody['soap:Fault']) {
super('Error in SOAP Payload');
const fault = soapBody['soap:Fault'];
this.errorCode = fault.faultcode;
this.errorMessage = fault.faultstring;
}
// Request Error
else if (response.status > 299) {
super('Error with SOAP Request');
}
// Fallback Error
else {
super('Unknown Error or Unhandled Request');
}
this.response = response;
this.JSON = soapBody;
}
}
71 changes: 68 additions & 3 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
* @param {object} obj Object to check
* @returns {boolean} true if is simple Object
*/
module.exports.isObject = (obj) =>
Object.prototype.toString.call(obj) === '[object Object]' ||
Object.prototype.toString.call(obj) === '[object Array]';
module.exports.isObject = (obj) => Object.prototype.toString.call(obj) === '[object Object]';

/**
* Method to check if Object passed is a valid payload for API calls
Expand All @@ -25,3 +23,70 @@ module.exports.isPayload = (obj) =>
*/
module.exports.isConnectionError = (code) =>
code && ['ETIMEDOUT', 'EHOSTUNREACH', 'ENOTFOUND', 'ECONNRESET', 'ECONNABORTED'].includes(code);

/**
* CustomError type for handling REST (including Auth) based errors
*
* @class RestError
* @extends {Error}
*/
module.exports.RestError = class RestError extends Error {
constructor(ex) {
// Expired Error
if (ex.response?.data?.message) {
super(ex.response.data.message);
this.code = ex.response.data.errorcode || ex.code;
}
// Unauthenticated
else if (ex.response?.data?.error_description) {
super(ex.response.data.error_description);
this.code = ex.response.data.error || ex.code;
} else {
super(ex.message);
this.code = ex.code;
}
this.response = ex.response;
this.name = this.constructor.name;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, RestError);
}
}
};

/**
* CustomError type for handling SOAP based errors
*
* @class SOAPError
* @extends {Error}
*/
module.exports.SOAPError = class SOAPError extends Error {
constructor(ex, response, soapBody) {
// Content Error
if (soapBody && ['Error', 'Has Errors'].includes(soapBody.OverallStatus)) {
super('One or more errors in the Results');
this.code = soapBody.OverallStatus;
}
// Payload Error
else if (soapBody && soapBody['soap:Fault']) {
const fault = soapBody['soap:Fault'];
super(fault.faultstring);
this.code = fault.faultcode;
}
// Request Error
else if (response?.status > 299) {
super('Error with SOAP Request');
this.code = response?.status;
}
// Fallback Error
else {
super(ex.message);
this.code = ex.code;
}
this.response = response;
this.json = soapBody;
this.name = this.constructor.name;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, SOAPError);
}
}
};
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
"main": "./lib/index.js",
"scripts": {
"test": "nyc --reporter=text mocha",
"lint": "eslint ./lib",
"lint:fix": "eslint ./lib --fix"
"lint": "eslint ./lib && eslint ./test",
"lint:fix": "eslint ./lib --fix && eslint ./test --fix"
},
"repository": {
"type": "git",
Expand Down
38 changes: 19 additions & 19 deletions test/auth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ const { defaultSdk, mock } = require('./utils.js');
const resources = require('./resources/auth.json');
const { isConnectionError } = require('../lib/util');

describe('auth', () => {
afterEach(() => {
describe('auth', function () {
afterEach(function () {
mock.reset();
});
it('should return an auth payload with token', async () => {
it('should return an auth payload with token', async function () {
//given
const { success } = resources;

Expand All @@ -20,7 +20,7 @@ describe('auth', () => {
assert.lengthOf(mock.history.post, 1);
return;
});
it('should return an auth payload with previous token and one request', async () => {
it('should return an auth payload with previous token and one request', async function () {
//given
const { success } = resources;
mock.onPost(success.url).reply(success.status, success.response);
Expand All @@ -33,7 +33,7 @@ describe('auth', () => {
assert.lengthOf(mock.history.post, 1);
return;
});
it('should return an unauthorized error', async () => {
it('should return an unauthorized error', async function () {
//given
const { unauthorized } = resources;
mock.onPost(unauthorized.url).reply(unauthorized.status, unauthorized.response);
Expand All @@ -49,10 +49,10 @@ describe('auth', () => {

return;
});
it('should return an incorrect account_id error', async () => {
it('should return an incorrect account_id error', async function () {
try {
//given
sfmc = new SDK({
new SDK({
client_id: 'XXXXX',
client_secret: 'YYYYYY',
auth_url: 'https://mct0l7nxfq2r988t1kxfy8sc47ma.auth.marketingcloudapis.com/',
Expand All @@ -68,10 +68,10 @@ describe('auth', () => {
}
return;
});
it('should return an incorrect auth_url error', async () => {
it('should return an incorrect auth_url error', async function () {
try {
//given
sfmc = new SDK({
new SDK({
client_id: 'XXXXX',
client_secret: 'YYYYYY',
auth_url: 'https://x.auth.marketingcloudapis.com/',
Expand All @@ -87,10 +87,10 @@ describe('auth', () => {
}
return;
});
it('should return an incorrect client_id error', async () => {
it('should return an incorrect client_id error', async function () {
try {
//given
sfmc = new SDK({
new SDK({
client_id: '',
client_secret: 'YYYYYY',
auth_url: 'https://mct0l7nxfq2r988t1kxfy8sc47ma.auth.marketingcloudapis.com/',
Expand All @@ -103,10 +103,10 @@ describe('auth', () => {
}
return;
});
it('should return an incorrect client_key error', async () => {
it('should return an incorrect client_key error', async function () {
try {
//given
sfmc = new SDK({
new SDK({
client_id: 'XXXXX',
client_secret: '',
auth_url: 'https://mct0l7nxfq2r988t1kxfy8sc47ma.auth.marketingcloudapis.com/',
Expand All @@ -119,10 +119,10 @@ describe('auth', () => {
}
return;
});
it('should return an invalid scope error', async () => {
it('should return an invalid scope error', async function () {
try {
//given
sfmc = new SDK({
new SDK({
client_id: 'XXXXX',
client_secret: 'YYYYYY',
auth_url: 'https://mct0l7nxfq2r988t1kxfy8sc47ma.auth.marketingcloudapis.com/',
Expand All @@ -136,10 +136,10 @@ describe('auth', () => {
}
return;
});
it('should return an invalid scope type error', async () => {
it('should return an invalid scope type error', async function () {
try {
//given
sfmc = new SDK({
new SDK({
client_id: 'XXXXX',
client_secret: 'YYYYYY',
auth_url: 'https://mct0l7nxfq2r988t1kxfy8sc47ma.auth.marketingcloudapis.com/',
Expand All @@ -154,7 +154,7 @@ describe('auth', () => {
return;
});

it('RETRY: should return an success, after a connection issues', async () => {
it('RETRY: should return an success, after a connection issues', async function () {
//given
const { success } = resources;

Expand All @@ -169,7 +169,7 @@ describe('auth', () => {
assert.lengthOf(mock.history.post, 2);
return;
});
it('FAILED RETRY: should return an error, after multiple connection issues', async () => {
it('FAILED RETRY: should return an error, after multiple connection issues', async function () {
//given
const { success } = resources;

Expand Down
Loading

0 comments on commit d2c1a68

Please sign in to comment.