mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 20:54:07 -08:00
feat: use ES2022 native error chaining to improve error reporting (#4431)
feat: use ES2022 native error chaining
This commit is contained in:
parent
3a9684df9f
commit
1f610b90f6
|
@ -717,8 +717,7 @@ export class ActiveWorkflowRunner {
|
||||||
// Run Error Workflow if defined
|
// Run Error Workflow if defined
|
||||||
const activationError = new WorkflowActivationError(
|
const activationError = new WorkflowActivationError(
|
||||||
`There was a problem with the trigger node "${node.name}", for that reason did the workflow had to be deactivated`,
|
`There was a problem with the trigger node "${node.name}", for that reason did the workflow had to be deactivated`,
|
||||||
error,
|
{ cause: error, node },
|
||||||
node,
|
|
||||||
);
|
);
|
||||||
this.executeErrorWorkflow(activationError, workflowData, mode);
|
this.executeErrorWorkflow(activationError, workflowData, mode);
|
||||||
|
|
||||||
|
|
|
@ -50,8 +50,11 @@ class ExternalHooksClass implements IExternalHooksClass {
|
||||||
const hookFile = require(hookFilePath) as IExternalHooksFileData;
|
const hookFile = require(hookFilePath) as IExternalHooksFileData;
|
||||||
this.loadHooks(hookFile);
|
this.loadHooks(hookFile);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
|
throw new Error(
|
||||||
throw new Error(`Problem loading external hook file "${hookFilePath}": ${error.message}`);
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/restrict-template-expressions
|
||||||
|
`Problem loading external hook file "${hookFilePath}": ${error.message}`,
|
||||||
|
{ cause: error as Error },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,8 +95,7 @@ export class ActiveWorkflows {
|
||||||
throw new WorkflowActivationError(
|
throw new WorkflowActivationError(
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
|
||||||
`There was a problem activating the workflow: "${error.message}"`,
|
`There was a problem activating the workflow: "${error.message}"`,
|
||||||
error,
|
{ cause: error as Error, node: triggerNode },
|
||||||
triggerNode,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -122,8 +121,7 @@ export class ActiveWorkflows {
|
||||||
throw new WorkflowActivationError(
|
throw new WorkflowActivationError(
|
||||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
|
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-member-access
|
||||||
`There was a problem activating the workflow: "${error.message}"`,
|
`There was a problem activating the workflow: "${error.message}"`,
|
||||||
error,
|
{ cause: error as Error, node: pollNode },
|
||||||
pollNode,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"extends": "../../tsconfig.json",
|
"extends": "../../tsconfig.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"lib": ["dom", "es2020"],
|
"lib": ["dom", "es2020", "es2022.error"],
|
||||||
"types": ["node", "jest"],
|
"types": ["node", "jest"],
|
||||||
// TODO: remove all options below this line
|
// TODO: remove all options below this line
|
||||||
"noImplicitReturns": false,
|
"noImplicitReturns": false,
|
||||||
|
|
|
@ -9,6 +9,7 @@ export class ExpressionError extends ExecutionBaseError {
|
||||||
constructor(
|
constructor(
|
||||||
message: string,
|
message: string,
|
||||||
options?: {
|
options?: {
|
||||||
|
cause?: Error;
|
||||||
causeDetailed?: string;
|
causeDetailed?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
descriptionTemplate?: string;
|
descriptionTemplate?: string;
|
||||||
|
@ -22,7 +23,7 @@ export class ExpressionError extends ExecutionBaseError {
|
||||||
type?: string;
|
type?: string;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
super(new Error(message));
|
super(message, { cause: options?.cause });
|
||||||
|
|
||||||
if (options?.description !== undefined) {
|
if (options?.description !== undefined) {
|
||||||
this.description = options.description;
|
this.description = options.description;
|
||||||
|
|
|
@ -56,29 +56,28 @@ const ERROR_STATUS_PROPERTIES = [
|
||||||
*/
|
*/
|
||||||
const ERROR_NESTING_PROPERTIES = ['error', 'err', 'response', 'body', 'data'];
|
const ERROR_NESTING_PROPERTIES = ['error', 'err', 'response', 'body', 'data'];
|
||||||
|
|
||||||
|
interface ExecutionBaseErrorOptions {
|
||||||
|
cause?: Error | JsonObject;
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class ExecutionBaseError extends Error {
|
export abstract class ExecutionBaseError extends Error {
|
||||||
description: string | null | undefined;
|
description: string | null | undefined;
|
||||||
|
|
||||||
cause: Error | JsonObject;
|
|
||||||
|
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
|
|
||||||
context: IDataObject = {};
|
context: IDataObject = {};
|
||||||
|
|
||||||
lineNumber: number | undefined;
|
lineNumber: number | undefined;
|
||||||
|
|
||||||
constructor(error: Error | ExecutionBaseError | JsonObject) {
|
constructor(message: string, { cause }: ExecutionBaseErrorOptions) {
|
||||||
super();
|
const options = cause instanceof Error ? { cause } : {};
|
||||||
|
super(message, options);
|
||||||
|
|
||||||
this.name = this.constructor.name;
|
this.name = this.constructor.name;
|
||||||
this.cause = error;
|
|
||||||
this.timestamp = Date.now();
|
this.timestamp = Date.now();
|
||||||
|
|
||||||
if (error.message) {
|
if (cause instanceof ExecutionBaseError) {
|
||||||
this.message = error.message as string;
|
this.context = cause.context;
|
||||||
}
|
|
||||||
|
|
||||||
if (error instanceof ExecutionBaseError) {
|
|
||||||
this.context = error.context;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,7 +90,8 @@ abstract class NodeError extends ExecutionBaseError {
|
||||||
node: INode;
|
node: INode;
|
||||||
|
|
||||||
constructor(node: INode, error: Error | JsonObject) {
|
constructor(node: INode, error: Error | JsonObject) {
|
||||||
super(error);
|
const message = error instanceof Error ? error.message : '';
|
||||||
|
super(message, { cause: error });
|
||||||
this.node = node;
|
this.node = node;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,23 +120,23 @@ abstract class NodeError extends ExecutionBaseError {
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
protected findProperty(
|
protected findProperty(
|
||||||
error: JsonObject,
|
jsonError: JsonObject,
|
||||||
potentialKeys: string[],
|
potentialKeys: string[],
|
||||||
traversalKeys: string[] = [],
|
traversalKeys: string[] = [],
|
||||||
): string | null {
|
): string | null {
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const key of potentialKeys) {
|
for (const key of potentialKeys) {
|
||||||
const value = error[key];
|
const value = jsonError[key];
|
||||||
if (value) {
|
if (value) {
|
||||||
if (typeof value === 'string') return value;
|
if (typeof value === 'string') return value;
|
||||||
if (typeof value === 'number') return value.toString();
|
if (typeof value === 'number') return value.toString();
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
const resolvedErrors: string[] = value
|
const resolvedErrors: string[] = value
|
||||||
.map((error) => {
|
.map((jsonError) => {
|
||||||
if (typeof error === 'string') return error;
|
if (typeof jsonError === 'string') return jsonError;
|
||||||
if (typeof error === 'number') return error.toString();
|
if (typeof jsonError === 'number') return jsonError.toString();
|
||||||
if (this.isTraversableObject(error)) {
|
if (this.isTraversableObject(jsonError)) {
|
||||||
return this.findProperty(error, potentialKeys);
|
return this.findProperty(jsonError, potentialKeys);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
|
@ -158,7 +158,7 @@ abstract class NodeError extends ExecutionBaseError {
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
for (const key of traversalKeys) {
|
for (const key of traversalKeys) {
|
||||||
const value = error[key];
|
const value = jsonError[key];
|
||||||
if (this.isTraversableObject(value)) {
|
if (this.isTraversableObject(value)) {
|
||||||
const property = this.findProperty(value, potentialKeys, traversalKeys);
|
const property = this.findProperty(value, potentialKeys, traversalKeys);
|
||||||
if (property) {
|
if (property) {
|
||||||
|
@ -209,35 +209,29 @@ abstract class NodeError extends ExecutionBaseError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface NodeOperationErrorOptions {
|
||||||
|
description?: string;
|
||||||
|
runIndex?: number;
|
||||||
|
itemIndex?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for instantiating an operational error, e.g. an invalid credentials error.
|
* Class for instantiating an operational error, e.g. an invalid credentials error.
|
||||||
*/
|
*/
|
||||||
export class NodeOperationError extends NodeError {
|
export class NodeOperationError extends NodeError {
|
||||||
lineNumber: number | undefined;
|
lineNumber: number | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(node: INode, error: Error | string, options: NodeOperationErrorOptions = {}) {
|
||||||
node: INode,
|
|
||||||
error: Error | string,
|
|
||||||
options?: { description?: string; runIndex?: number; itemIndex?: number },
|
|
||||||
) {
|
|
||||||
if (typeof error === 'string') {
|
if (typeof error === 'string') {
|
||||||
error = new Error(error);
|
error = new Error(error);
|
||||||
}
|
}
|
||||||
super(node, error);
|
super(node, error);
|
||||||
|
|
||||||
if (options?.description) {
|
|
||||||
this.description = options.description;
|
this.description = options.description;
|
||||||
}
|
|
||||||
|
|
||||||
if (options?.runIndex !== undefined) {
|
|
||||||
this.context.runIndex = options.runIndex;
|
this.context.runIndex = options.runIndex;
|
||||||
}
|
|
||||||
|
|
||||||
if (options?.itemIndex !== undefined) {
|
|
||||||
this.context.itemIndex = options.itemIndex;
|
this.context.itemIndex = options.itemIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const STATUS_CODE_MESSAGES: IStatusCodeMessages = {
|
const STATUS_CODE_MESSAGES: IStatusCodeMessages = {
|
||||||
'4XX': 'Your request is invalid or could not be processed by the service',
|
'4XX': 'Your request is invalid or could not be processed by the service',
|
||||||
|
@ -258,6 +252,12 @@ const STATUS_CODE_MESSAGES: IStatusCodeMessages = {
|
||||||
|
|
||||||
const UNKNOWN_ERROR_MESSAGE = 'UNKNOWN ERROR - check the detailed error for more information';
|
const UNKNOWN_ERROR_MESSAGE = 'UNKNOWN ERROR - check the detailed error for more information';
|
||||||
|
|
||||||
|
interface NodeApiErrorOptions extends NodeOperationErrorOptions {
|
||||||
|
message?: string;
|
||||||
|
httpCode?: string;
|
||||||
|
parseXml?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for instantiating an error in an API response, e.g. a 404 Not Found response,
|
* 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.
|
* with an HTTP error code, an error message and a description.
|
||||||
|
@ -268,21 +268,7 @@ export class NodeApiError extends NodeError {
|
||||||
constructor(
|
constructor(
|
||||||
node: INode,
|
node: INode,
|
||||||
error: JsonObject,
|
error: JsonObject,
|
||||||
{
|
{ message, description, httpCode, parseXml, runIndex, itemIndex }: NodeApiErrorOptions = {},
|
||||||
message,
|
|
||||||
description,
|
|
||||||
httpCode,
|
|
||||||
parseXml,
|
|
||||||
runIndex,
|
|
||||||
itemIndex,
|
|
||||||
}: {
|
|
||||||
message?: string;
|
|
||||||
description?: string;
|
|
||||||
httpCode?: string;
|
|
||||||
parseXml?: boolean;
|
|
||||||
runIndex?: number;
|
|
||||||
itemIndex?: number;
|
|
||||||
} = {},
|
|
||||||
) {
|
) {
|
||||||
super(node, error);
|
super(node, error);
|
||||||
if (error.error) {
|
if (error.error) {
|
||||||
|
|
|
@ -277,12 +277,12 @@ export class RoutingNode {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
throw new NodeOperationError(
|
throw new NodeOperationError(this.node, error, {
|
||||||
this.node,
|
runIndex,
|
||||||
`The rootProperty "${action.properties.property}" could not be found on item.`,
|
itemIndex,
|
||||||
{ runIndex, itemIndex },
|
description: `The rootProperty "${action.properties.property}" could not be found on item.`,
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (action.type === 'limit') {
|
if (action.type === 'limit') {
|
||||||
|
|
|
@ -1,19 +1,27 @@
|
||||||
import { INode } from './Interfaces';
|
import { INode } from './Interfaces';
|
||||||
import { ExecutionBaseError } from './NodeErrors';
|
import { ExecutionBaseError } from './NodeErrors';
|
||||||
|
|
||||||
|
interface WorkflowActivationErrorOptions {
|
||||||
|
cause?: Error;
|
||||||
|
node?: INode;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for instantiating an workflow activation error
|
* Class for instantiating an workflow activation error
|
||||||
*/
|
*/
|
||||||
export class WorkflowActivationError extends ExecutionBaseError {
|
export class WorkflowActivationError extends ExecutionBaseError {
|
||||||
node: INode | undefined;
|
node: INode | undefined;
|
||||||
|
|
||||||
constructor(message: string, error: Error, node?: INode) {
|
constructor(message: string, { cause, node }: WorkflowActivationErrorOptions) {
|
||||||
super(error);
|
let error = cause as Error;
|
||||||
|
if (cause instanceof ExecutionBaseError) {
|
||||||
|
error = new Error(cause.message);
|
||||||
|
error.constructor = cause.constructor;
|
||||||
|
error.name = cause.name;
|
||||||
|
error.stack = cause.stack;
|
||||||
|
}
|
||||||
|
super(message, { cause: error });
|
||||||
this.node = node;
|
this.node = node;
|
||||||
this.cause = {
|
|
||||||
message: error.message,
|
|
||||||
stack: error.stack as string,
|
|
||||||
};
|
|
||||||
this.message = message;
|
this.message = message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"target": "es2019",
|
"target": "es2019",
|
||||||
"lib": ["es2019", "es2020"],
|
"lib": ["es2019", "es2020", "es2022.error"],
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"useUnknownInCatchVariables": true,
|
"useUnknownInCatchVariables": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
|
Loading…
Reference in a new issue