mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
✨ Improve node error handling (#1309)
* Add path mapping and response error interfaces * Add error handling and throwing functionality * Refactor error handling into a single function * Re-implement error handling in Hacker News node * Fix linting details * Re-implement error handling in Spotify node * Re-implement error handling in G Suite Admin node * 🚧 create basic setup NodeError * 🚧 add httpCodes * 🚧 add path priolist * 🚧 handle statusCode in error, adjust interfaces * 🚧 fixing type issues w/Ivan * 🚧 add error exploration * 👔 fix linter issues * 🔧 improve object check * 🚧 remove path passing from NodeApiError * 🚧 add multi error + refactor findProperty method * 👔 allow any * 🔧 handle multi error message callback * ⚡ change return type of callback * ⚡ add customCallback to MultiError * 🚧 refactor to use INode * 🔨 handle arrays, continue search after first null property found * 🚫 refactor method access * 🚧 setup NodeErrorView * ⚡ change timestamp to Date.now * 📚 Add documentation for methods and constants * 🚧 change message setting * 🚚 move NodeErrors to workflow * ✨ add new ErrorView for Nodes * 🎨 improve error notification * 🎨 refactor interfaces * ⚡ add WorkflowOperationError, refactor error throwing * 👕 fix linter issues * 🎨 rename param * 🐛 fix handling normal errors * ⚡ add usage of NodeApiError * 🎨 fix throw new error instead of constructor * 🎨 remove unnecessary code/comments * 🎨 adjusted spacing + updated status messages * 🎨 fix tab indentation * ✨ Replace current errors with custom errors (#1576) * ⚡ Introduce NodeApiError in catch blocks * ⚡ Introduce NodeOperationError in nodes * ⚡ Add missing errors and remove incompatible * ⚡ Fix NodeOperationError in incompatible nodes * 🔧 Adjust error handling in missed nodes PayPal, FileMaker, Reddit, Taiga and Facebook Graph API nodes * 🔨 Adjust Strava Trigger node error handling * 🔨 Adjust AWS nodes error handling * 🔨 Remove duplicate instantiation of NodeApiError * 🐛 fix strava trigger node error handling * Add XML parsing to NodeApiError constructor (#1633) * 🐛 Remove type annotation from catch variable * ✨ Add XML parsing to NodeApiError * ⚡ Simplify error handling in Rekognition node * ⚡ Pass in XML flag in generic functions * 🔥 Remove try/catch wrappers at call sites * 🔨 Refactor setting description from XML * 🔨 Refactor let to const in resource loaders * ⚡ Find property in parsed XML * ⚡ Change let to const * 🔥 Remove unneeded try/catch block * 👕 Fix linting issues * 🐛 Fix errors from merge conflict resolution * ⚡ Add custom errors to latest contributions * 👕 Fix linting issues * ⚡ Refactor MongoDB helpers for custom errors * 🐛 Correct custom error type * ⚡ Apply feedback to A nodes * ⚡ Apply feedback to missed A node * ⚡ Apply feedback to B-D nodes * ⚡ Apply feedback to E-F nodes * ⚡ Apply feedback to G nodes * ⚡ Apply feedback to H-L nodes * ⚡ Apply feedback to M nodes * ⚡ Apply feedback to P nodes * ⚡ Apply feedback to R nodes * ⚡ Apply feedback to S nodes * ⚡ Apply feedback to T nodes * ⚡ Apply feedback to V-Z nodes * ⚡ Add HTTP code to iterable node error * 🔨 Standardize e as error * 🔨 Standardize err as error * ⚡ Fix error handling for non-standard nodes Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com> Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com> Co-authored-by: Ben Hesseldieck <1849459+BHesseldieck@users.noreply.github.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { Workflow } from './Workflow';
|
||||
import { WorkflowHooks } from './WorkflowHooks';
|
||||
import { WorkflowOperationError } from './WorkflowErrors';
|
||||
import { NodeApiError, NodeOperationError } from './NodeErrors';
|
||||
import * as express from 'express';
|
||||
|
||||
export type IAllExecuteFunctions = IExecuteFunctions | IExecuteSingleFunctions | IHookFunctions | ILoadOptionsFunctions | IPollFunctions | ITriggerFunctions | IWebhookFunctions;
|
||||
@@ -32,11 +34,7 @@ export interface IConnection {
|
||||
index: number;
|
||||
}
|
||||
|
||||
export interface IExecutionError {
|
||||
message: string;
|
||||
node?: string;
|
||||
stack?: string;
|
||||
}
|
||||
export type ExecutionError = WorkflowOperationError | NodeOperationError | NodeApiError;
|
||||
|
||||
// Get used to gives nodes access to credentials
|
||||
export interface IGetCredentials {
|
||||
@@ -660,7 +658,7 @@ export interface IRunExecutionData {
|
||||
runNodeFilter?: string[];
|
||||
};
|
||||
resultData: {
|
||||
error?: IExecutionError;
|
||||
error?: ExecutionError;
|
||||
runData: IRunData;
|
||||
lastNodeExecuted?: string;
|
||||
};
|
||||
@@ -683,7 +681,7 @@ export interface ITaskData {
|
||||
startTime: number;
|
||||
executionTime: number;
|
||||
data?: ITaskDataConnections;
|
||||
error?: IExecutionError;
|
||||
error?: ExecutionError;
|
||||
}
|
||||
|
||||
|
||||
@@ -761,7 +759,14 @@ export interface IWorkflowHooksOptionalParameters {
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
|
||||
export interface IWorkflowSettings {
|
||||
[key: string]: IDataObject | string | number | boolean | undefined;
|
||||
}
|
||||
|
||||
export interface IRawErrorObject {
|
||||
[key: string]: string | object | number | boolean | undefined | null | string[] | object[] | number[] | boolean[];
|
||||
}
|
||||
|
||||
export interface IStatusCodeMessages {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
241
packages/workflow/src/NodeErrors.ts
Normal file
241
packages/workflow/src/NodeErrors.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import { INode, IRawErrorObject, IStatusCodeMessages} 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 | IRawErrorObject;
|
||||
node: INode;
|
||||
timestamp: number;
|
||||
|
||||
constructor(node: INode, error: Error | IRawErrorObject) {
|
||||
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)
|
||||
*
|
||||
* 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 {IRawErrorObject} error
|
||||
* @param {string[]} potentialKeys
|
||||
* @param {string[]} traversalKeys
|
||||
* @returns {string | null}
|
||||
*/
|
||||
protected findProperty(
|
||||
error: IRawErrorObject,
|
||||
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, traversalKeys);
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((errorValue: string | null) => errorValue !== null);
|
||||
|
||||
if (resolvedErrors.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return resolvedErrors.join(' | ');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of traversalKeys) {
|
||||
if (this.isTraversableObject(error[key])) {
|
||||
const property = this.findProperty(error[key] as IRawErrorObject, 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.
|
||||
*/
|
||||
private isTraversableObject(value: any): value is IRawErrorObject { // tslint:disable-line:no-any
|
||||
return value && typeof value === 'object' && !Array.isArray(value) && !!Object.keys(value).length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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: IRawErrorObject,
|
||||
{ message, description, httpCode, parseXml }: { message?: string, description?: string, httpCode?: string, parseXml?: boolean } = {},
|
||||
) {
|
||||
super(node, error);
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -937,6 +937,7 @@ export class Workflow {
|
||||
// The node did already fail. So throw an error here that it displays and logs it correctly.
|
||||
// Does get used by webhook and trigger nodes in case they throw an error that it is possible
|
||||
// to log the error and display in Editor-UI.
|
||||
|
||||
const error = new Error(runExecutionData.resultData.error.message);
|
||||
error.stack = runExecutionData.resultData.error.stack;
|
||||
throw error;
|
||||
|
||||
16
packages/workflow/src/WorkflowErrors.ts
Normal file
16
packages/workflow/src/WorkflowErrors.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { INode } from '.';
|
||||
|
||||
/**
|
||||
* Class for instantiating an operational error, e.g. a timeout error.
|
||||
*/
|
||||
export class WorkflowOperationError extends Error {
|
||||
node: INode | undefined;
|
||||
timestamp: number;
|
||||
|
||||
constructor(message: string, node?: INode, ) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
this.node = node;
|
||||
this.timestamp = Date.now();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
export * from './Interfaces';
|
||||
export * from './Expression';
|
||||
export * from './NodeErrors';
|
||||
export * from './Workflow';
|
||||
export * from './WorkflowDataProxy';
|
||||
export * from './WorkflowErrors';
|
||||
export * from './WorkflowHooks';
|
||||
|
||||
import * as NodeHelpers from './NodeHelpers';
|
||||
|
||||
Reference in New Issue
Block a user