Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standardized Error Messages returned where possible #108

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 () {
JoernBerkefeld marked this conversation as resolved.
Show resolved Hide resolved
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