n8n/packages/workflow/src/NodeErrors.ts
Iván Ovejero e45d173c45
🐛 Fix circular references removal (#1901)
Circular references removed only for the request library error.
2021-06-16 12:41:56 +02:00

276 lines
7.9 KiB
TypeScript

import { INode, IStatusCodeMessages, JsonObject} from '.';
import { parseString } from 'xml2js';
/**
* Top-level properties where an error message can be found in an API response.
*/
const ERROR_MESSAGE_PROPERTIES = [
'error',
'message',
'Message',
'msg',
'messages',
'description',
'reason',
'detail',
'details',
'errors',
'errorMessage',
'errorMessages',
'ErrorMessage',
'error_message',
'_error_message',
'errorDescription',
'error_description',
'error_summary',
'title',
'text',
'field',
'err',
'type',
];
/**
* Top-level properties where an HTTP error code can be found in an API response.
*/
const ERROR_STATUS_PROPERTIES = ['statusCode', 'status', 'code', 'status_code', 'errorCode', 'error_code'];
/**
* Properties where a nested object can be found in an API response.
*/
const ERROR_NESTING_PROPERTIES = ['error', 'err', 'response', 'body', 'data'];
/**
* Base class for specific NodeError-types, with functionality for finding
* a value recursively inside an error object.
*/
abstract class NodeError extends Error {
description: string | null | undefined;
cause: Error | JsonObject;
node: INode;
timestamp: number;
constructor(node: INode, error: Error | JsonObject) {
super();
this.name = this.constructor.name;
this.cause = error;
this.node = node;
this.timestamp = Date.now();
if (error.message) {
this.message = error.message as string;
}
}
/**
* Finds property through exploration based on potential keys and traversal keys.
* Depth-first approach.
*
* This method iterates over `potentialKeys` and, if the value at the key is a
* truthy value, the type of the value is checked:
* (1) if a string or number, the value is returned as a string; or
* (2) if an array,
* its string or number elements are collected as a long string,
* its object elements are traversed recursively (restart this function
* with each object as a starting point), or
* (3) if it is an object, it traverses the object and nested ones recursively
* based on the `potentialKeys` and returns a string if found.
*
* If nothing found via `potentialKeys` this method iterates over `traversalKeys` and
* if the value at the key is a traversable object, it restarts with the object as the
* new starting point (recursion).
* If nothing found for any of the `traversalKeys`, exploration continues with remaining
* `traversalKeys`.
*
* Otherwise, if all the paths have been exhausted and no value is eligible, `null` is
* returned.
*
* @param {JsonObject} error
* @param {string[]} potentialKeys
* @param {string[]} traversalKeys
* @returns {string | null}
*/
protected findProperty(
error: JsonObject,
potentialKeys: string[],
traversalKeys: string[] = [],
): string | null {
for(const key of potentialKeys) {
if (error[key]) {
if (typeof error[key] === 'string') return error[key] as string;
if (typeof error[key] === 'number') return error[key]!.toString();
if (Array.isArray(error[key])) {
// @ts-ignore
const resolvedErrors: string[] = error[key].map((error) => {
if (typeof error === 'string') return error;
if (typeof error === 'number') return error.toString();
if (this.isTraversableObject(error)) {
return this.findProperty(error, potentialKeys);
}
return null;
})
.filter((errorValue: string | null) => errorValue !== null);
if (resolvedErrors.length === 0) {
return null;
}
return resolvedErrors.join(' | ');
}
if (this.isTraversableObject(error[key])) {
const property = this.findProperty(error[key] as JsonObject, potentialKeys);
if (property) {
return property;
}
}
}
}
for (const key of traversalKeys) {
if (this.isTraversableObject(error[key])) {
const property = this.findProperty(error[key] as JsonObject, potentialKeys, traversalKeys);
if (property) {
return property;
}
}
}
return null;
}
/**
* Check if a value is an object with at least one key, i.e. it can be traversed.
*/
protected isTraversableObject(value: any): value is JsonObject { // tslint:disable-line:no-any
return value && typeof value === 'object' && !Array.isArray(value) && !!Object.keys(value).length;
}
/**
* Remove circular references from objects.
*/
protected removeCircularRefs(obj: JsonObject, seen = new Set()) {
seen.add(obj);
Object.entries(obj).forEach(([key, value]) => {
if (this.isTraversableObject(value)) {
seen.has(value) ? obj[key] = { circularReference: true } : this.removeCircularRefs(value, seen);
return;
}
if (Array.isArray(value)) {
value.forEach((val, index) => {
if (seen.has(val)) {
value[index] = { circularReference: true };
return;
}
if (this.isTraversableObject(val)) {
this.removeCircularRefs(val, seen);
}
});
}
});
}
}
/**
* Class for instantiating an operational error, e.g. an invalid credentials error.
*/
export class NodeOperationError extends NodeError {
constructor(node: INode, error: Error | string) {
if (typeof error === 'string') {
error = new Error(error);
}
super(node, error);
}
}
const STATUS_CODE_MESSAGES: IStatusCodeMessages = {
'4XX': 'Your request is invalid or could not be processed by the service',
'400': 'Bad request - please check your parameters',
'401': 'Authorization failed - please check your credentials',
'402': 'Payment required - perhaps check your payment details?',
'403': 'Forbidden - perhaps check your credentials?',
'404': 'The resource you are requesting could not be found',
'405': 'Method not allowed - please check you are using the right HTTP method',
'429': 'The service is receiving too many requests from you! Perhaps take a break?',
'5XX': 'The service failed to process your request',
'500': 'The service was not able to process your request',
'502': 'Bad gateway - the service failed to handle your request',
'503': 'Service unavailable - perhaps try again later?',
'504': 'Gateway timed out - perhaps try again later?',
};
const UNKNOWN_ERROR_MESSAGE = 'UNKNOWN ERROR - check the detailed error for more information';
/**
* Class for instantiating an error in an API response, e.g. a 404 Not Found response,
* with an HTTP error code, an error message and a description.
*/
export class NodeApiError extends NodeError {
httpCode: string | null;
constructor(
node: INode,
error: JsonObject,
{ message, description, httpCode, parseXml }: { message?: string, description?: string, httpCode?: string, parseXml?: boolean } = {},
) {
super(node, error);
if (error.error) { // only for request library error
this.removeCircularRefs(error.error as JsonObject);
}
if (message) {
this.message = message;
this.description = description;
this.httpCode = httpCode ?? null;
return;
}
this.httpCode = this.findProperty(error, ERROR_STATUS_PROPERTIES, ERROR_NESTING_PROPERTIES);
this.setMessage();
if (parseXml) {
this.setDescriptionFromXml(error.error as string);
return;
}
this.description = this.findProperty(error, ERROR_MESSAGE_PROPERTIES, ERROR_NESTING_PROPERTIES);
}
private setDescriptionFromXml(xml: string) {
parseString(xml, { explicitArray: false }, (_, result) => {
if (!result) return;
const topLevelKey = Object.keys(result)[0];
this.description = this.findProperty(result[topLevelKey], ERROR_MESSAGE_PROPERTIES, ['Error'].concat(ERROR_NESTING_PROPERTIES));
});
}
/**
* Set the error's message based on the HTTP status code.
*
* @returns {void}
*/
private setMessage() {
if (!this.httpCode) {
this.httpCode = null;
this.message = UNKNOWN_ERROR_MESSAGE;
return;
}
if (STATUS_CODE_MESSAGES[this.httpCode]) {
this.message = STATUS_CODE_MESSAGES[this.httpCode];
return;
}
switch (this.httpCode.charAt(0)) {
case '4':
this.message = STATUS_CODE_MESSAGES['4XX'];
break;
case '5':
this.message = STATUS_CODE_MESSAGES['5XX'];
break;
default:
this.message = UNKNOWN_ERROR_MESSAGE;
}
}
}