From 7749b855f300a2271c128cfc77a156a830483081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 27 Nov 2023 10:26:38 +0100 Subject: [PATCH 1/6] refactor(core): Reorganize error hierarchy in `core` and `workflow` packages (no-changelog) --- packages/cli/src/ActiveWebhooks.ts | 4 +- packages/cli/src/ActiveWorkflowRunner.ts | 4 +- packages/cli/src/ErrorReporting.ts | 4 +- .../core/src/BinaryData/BinaryData.service.ts | 5 +- .../core/src/BinaryData/FileSystem.manager.ts | 11 +- packages/core/src/BinaryData/errors.ts | 11 - ...cutionMetadata.ts => ExecutionMetadata.ts} | 19 +- packages/core/src/NodeExecuteFunctions.ts | 11 +- packages/core/src/WorkflowExecute.ts | 30 +- .../src/errors/abstract/binary-data.error.ts | 3 + .../src/errors/abstract/filesystem.error.ts | 7 + .../src/errors/disallowed-filepath.error.ts | 7 + .../core/src/errors/file-not-found.error.ts | 7 + packages/core/src/errors/filesystem.errors.ts | 21 - packages/core/src/errors/index.ts | 6 +- .../invalid-execution-metadata.error.ts | 13 + .../core/src/errors/invalid-manager.error.ts | 7 + .../core/src/errors/invalid-mode.error.ts | 8 + packages/core/src/index.ts | 1 + .../test/WorkflowExecutionMetadata.test.ts | 12 +- packages/workflow/src/ErrorReporterProxy.ts | 4 +- packages/workflow/src/Expression.ts | 3 +- .../src/Extensions/ArrayExtensions.ts | 3 +- .../workflow/src/Extensions/DateExtensions.ts | 2 +- .../src/Extensions/ExpressionExtension.ts | 2 +- .../src/Extensions/ExtendedFunctions.ts | 3 +- .../src/Extensions/NumberExtensions.ts | 2 +- .../src/Extensions/ObjectExtensions.ts | 2 +- .../src/Extensions/StringExtensions.ts | 14 +- packages/workflow/src/Interfaces.ts | 11 +- packages/workflow/src/NodeErrors.ts | 512 ------------------ packages/workflow/src/RoutingNode.ts | 8 +- packages/workflow/src/WorkflowDataProxy.ts | 2 +- packages/workflow/src/WorkflowErrors.ts | 42 -- .../errors/abstract/execution-base.error.ts | 47 ++ .../src/errors/abstract/node.error.ts | 168 ++++++ ...portable.error.ts => application.error.ts} | 9 +- .../errors/cli-subworkflow-operation.error.ts | 3 + .../src/errors/expression-extension.error.ts | 3 + .../expression.error.ts} | 6 +- packages/workflow/src/errors/index.ts | 16 +- .../workflow/src/errors/node-api.error.ts | 267 +++++++++ .../src/errors/node-operation.error.ts | 34 ++ .../workflow/src/errors/node-ssl.error.ts | 7 + .../src/errors/subworkflow-operation.error.ts | 18 + .../src/errors/webhook-taken.error.ts | 10 + .../workflow-activation.error.ts} | 15 +- .../src/errors/workflow-deactivation.error.ts | 3 + .../src/errors/workflow-operation.error.ts | 23 + packages/workflow/src/index.ts | 4 - packages/workflow/test/Expression.test.ts | 2 +- packages/workflow/test/NodeErrors.test.ts | 2 +- .../workflow/test/WorkflowDataProxy.test.ts | 2 +- 53 files changed, 750 insertions(+), 690 deletions(-) delete mode 100644 packages/core/src/BinaryData/errors.ts rename packages/core/src/{WorkflowExecutionMetadata.ts => ExecutionMetadata.ts} (79%) create mode 100644 packages/core/src/errors/abstract/binary-data.error.ts create mode 100644 packages/core/src/errors/abstract/filesystem.error.ts create mode 100644 packages/core/src/errors/disallowed-filepath.error.ts create mode 100644 packages/core/src/errors/file-not-found.error.ts delete mode 100644 packages/core/src/errors/filesystem.errors.ts create mode 100644 packages/core/src/errors/invalid-execution-metadata.error.ts create mode 100644 packages/core/src/errors/invalid-manager.error.ts create mode 100644 packages/core/src/errors/invalid-mode.error.ts delete mode 100644 packages/workflow/src/NodeErrors.ts delete mode 100644 packages/workflow/src/WorkflowErrors.ts create mode 100644 packages/workflow/src/errors/abstract/execution-base.error.ts create mode 100644 packages/workflow/src/errors/abstract/node.error.ts rename packages/workflow/src/errors/{reportable.error.ts => application.error.ts} (63%) create mode 100644 packages/workflow/src/errors/cli-subworkflow-operation.error.ts create mode 100644 packages/workflow/src/errors/expression-extension.error.ts rename packages/workflow/src/{ExpressionError.ts => errors/expression.error.ts} (85%) create mode 100644 packages/workflow/src/errors/node-api.error.ts create mode 100644 packages/workflow/src/errors/node-operation.error.ts create mode 100644 packages/workflow/src/errors/node-ssl.error.ts create mode 100644 packages/workflow/src/errors/subworkflow-operation.error.ts create mode 100644 packages/workflow/src/errors/webhook-taken.error.ts rename packages/workflow/src/{WorkflowActivationError.ts => errors/workflow-activation.error.ts} (63%) create mode 100644 packages/workflow/src/errors/workflow-deactivation.error.ts create mode 100644 packages/workflow/src/errors/workflow-operation.error.ts diff --git a/packages/cli/src/ActiveWebhooks.ts b/packages/cli/src/ActiveWebhooks.ts index a271021471412..54705a0c17222 100644 --- a/packages/cli/src/ActiveWebhooks.ts +++ b/packages/cli/src/ActiveWebhooks.ts @@ -6,7 +6,7 @@ import type { WorkflowActivateMode, WorkflowExecuteMode, } from 'n8n-workflow'; -import { WebhookPathAlreadyTakenError } from 'n8n-workflow'; +import { WebhookTakenError } from 'n8n-workflow'; import * as NodeExecuteFunctions from 'n8n-core'; @Service() @@ -46,7 +46,7 @@ export class ActiveWebhooks { // check that there is not a webhook already registered with that path/method if (this.webhookUrls[webhookKey] && !webhookData.webhookId) { - throw new WebhookPathAlreadyTakenError(webhookData.node); + throw new WebhookTakenError(webhookData.node); } if (this.workflowWebhooks[webhookData.workflowId] === undefined) { diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 5c2807d769401..2f55afda0eb78 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -29,7 +29,7 @@ import { Workflow, WorkflowActivationError, ErrorReporterProxy as ErrorReporter, - WebhookPathAlreadyTakenError, + WebhookTakenError, } from 'n8n-workflow'; import type express from 'express'; @@ -403,7 +403,7 @@ export class ActiveWorkflowRunner implements IWebhookManager { // TODO check if there is standard error code for duplicate key violation that works // with all databases if (error instanceof Error && error.name === 'QueryFailedError') { - error = new WebhookPathAlreadyTakenError(webhook.node, error); + error = new WebhookTakenError(webhook.node, error); } else if (error.detail) { // it's a error running the webhook methods (checkExists, create) error.message = error.detail; diff --git a/packages/cli/src/ErrorReporting.ts b/packages/cli/src/ErrorReporting.ts index cc193a4900407..7733f81665c9c 100644 --- a/packages/cli/src/ErrorReporting.ts +++ b/packages/cli/src/ErrorReporting.ts @@ -1,6 +1,6 @@ import { createHash } from 'crypto'; import config from '@/config'; -import { ErrorReporterProxy, ExecutionBaseError, ReportableError } from 'n8n-workflow'; +import { ErrorReporterProxy, ApplicationError, ExecutionBaseError } from 'n8n-workflow'; let initialized = false; @@ -42,7 +42,7 @@ export const initErrorHandling = async () => { if (originalException instanceof ExecutionBaseError && originalException.severity === 'warning') return null; - if (originalException instanceof ReportableError) { + if (originalException instanceof ApplicationError) { const { level, extra } = originalException; if (level === 'warning') return null; event.level = level; diff --git a/packages/core/src/BinaryData/BinaryData.service.ts b/packages/core/src/BinaryData/BinaryData.service.ts index b3377f8181327..ad8a071b8f076 100644 --- a/packages/core/src/BinaryData/BinaryData.service.ts +++ b/packages/core/src/BinaryData/BinaryData.service.ts @@ -2,12 +2,13 @@ import { readFile, stat } from 'node:fs/promises'; import prettyBytes from 'pretty-bytes'; import Container, { Service } from 'typedi'; import { BINARY_ENCODING } from 'n8n-workflow'; -import { UnknownManagerError, InvalidModeError } from './errors'; +import { InvalidModeError } from '../errors/invalid-mode.error'; import { areConfigModes, toBuffer } from './utils'; import type { Readable } from 'stream'; import type { BinaryData } from './types'; import type { INodeExecutionData, IBinaryData } from 'n8n-workflow'; +import { InvalidManagerError } from '../errors/invalid-manager.error'; @Service() export class BinaryDataService { @@ -241,6 +242,6 @@ export class BinaryDataService { if (manager) return manager; - throw new UnknownManagerError(mode); + throw new InvalidManagerError(mode); } } diff --git a/packages/core/src/BinaryData/FileSystem.manager.ts b/packages/core/src/BinaryData/FileSystem.manager.ts index 23b26dee09986..1e076ad4c70fe 100644 --- a/packages/core/src/BinaryData/FileSystem.manager.ts +++ b/packages/core/src/BinaryData/FileSystem.manager.ts @@ -4,10 +4,11 @@ import path from 'node:path'; import { v4 as uuid } from 'uuid'; import { jsonParse } from 'n8n-workflow'; import { assertDir, doesNotExist } from './utils'; -import { BinaryFileNotFoundError, InvalidPathError } from '../errors'; +import { DisallowedFilepathError } from '../errors/disallowed-filepath.error'; import type { Readable } from 'stream'; import type { BinaryData } from './types'; +import { FileNotFoundError } from '../errors/file-not-found.error'; const EXECUTION_ID_EXTRACTOR = /^(\w+)(?:[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12})$/; @@ -47,7 +48,7 @@ export class FileSystemManager implements BinaryData.Manager { const filePath = this.resolvePath(fileId); if (await doesNotExist(filePath)) { - throw new BinaryFileNotFoundError(filePath); + throw new FileNotFoundError(filePath); } return createReadStream(filePath, { highWaterMark: chunkSize }); @@ -57,7 +58,7 @@ export class FileSystemManager implements BinaryData.Manager { const filePath = this.resolvePath(fileId); if (await doesNotExist(filePath)) { - throw new BinaryFileNotFoundError(filePath); + throw new FileNotFoundError(filePath); } return fs.readFile(filePath); @@ -171,7 +172,7 @@ export class FileSystemManager implements BinaryData.Manager { const returnPath = path.join(this.storagePath, ...args); if (path.relative(this.storagePath, returnPath).startsWith('..')) { - throw new InvalidPathError(returnPath); + throw new DisallowedFilepathError(returnPath); } return returnPath; @@ -190,7 +191,7 @@ export class FileSystemManager implements BinaryData.Manager { const stats = await fs.stat(filePath); return stats.size; } catch (error) { - throw new BinaryFileNotFoundError(filePath); + throw new FileNotFoundError(filePath); } } } diff --git a/packages/core/src/BinaryData/errors.ts b/packages/core/src/BinaryData/errors.ts deleted file mode 100644 index 31b3adc1e4599..0000000000000 --- a/packages/core/src/BinaryData/errors.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { CONFIG_MODES } from './utils'; - -export class InvalidModeError extends Error { - message = `Invalid binary data mode. Valid modes: ${CONFIG_MODES.join(', ')}`; -} - -export class UnknownManagerError extends Error { - constructor(mode: string) { - super(`No binary data manager found for: ${mode}`); - } -} diff --git a/packages/core/src/WorkflowExecutionMetadata.ts b/packages/core/src/ExecutionMetadata.ts similarity index 79% rename from packages/core/src/WorkflowExecutionMetadata.ts rename to packages/core/src/ExecutionMetadata.ts index 79c36527535de..50c1ccbc11024 100644 --- a/packages/core/src/WorkflowExecutionMetadata.ts +++ b/packages/core/src/ExecutionMetadata.ts @@ -1,20 +1,9 @@ import type { IRunExecutionData } from 'n8n-workflow'; import { LoggerProxy as Logger } from 'n8n-workflow'; +import { InvalidExecutionMetadata } from './errors/invalid-execution-metadata.error'; export const KV_LIMIT = 10; -export class ExecutionMetadataValidationError extends Error { - constructor( - public type: 'key' | 'value', - key: unknown, - message?: string, - options?: ErrorOptions, - ) { - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - super(message ?? `Custom data ${type}s must be a string (key "${key}")`, options); - } -} - export function setWorkflowExecutionMetadata( executionData: IRunExecutionData, key: string, @@ -31,17 +20,17 @@ export function setWorkflowExecutionMetadata( return; } if (typeof key !== 'string') { - throw new ExecutionMetadataValidationError('key', key); + throw new InvalidExecutionMetadata('key', key); } if (key.replace(/[A-Za-z0-9_]/g, '').length !== 0) { - throw new ExecutionMetadataValidationError( + throw new InvalidExecutionMetadata( 'key', key, `Custom date key can only contain characters "A-Za-z0-9_" (key "${key}")`, ); } if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'bigint') { - throw new ExecutionMetadataValidationError('value', key); + throw new InvalidExecutionMetadata('value', key); } const val = String(value); if (key.length > 50) { diff --git a/packages/core/src/NodeExecuteFunctions.ts b/packages/core/src/NodeExecuteFunctions.ts index 2f0ec637d4491..18bf12dbd81e6 100644 --- a/packages/core/src/NodeExecuteFunctions.ts +++ b/packages/core/src/NodeExecuteFunctions.ts @@ -38,7 +38,6 @@ import type { BinaryHelperFunctions, ConnectionTypes, ContextType, - ExecutionError, FieldType, FileSystemHelperFunctions, FunctionsBase, @@ -101,7 +100,7 @@ import { NodeApiError, NodeHelpers, NodeOperationError, - NodeSSLError, + NodeSslError, OAuth2GrantType, WorkflowDataProxy, createDeferredPromise, @@ -143,7 +142,7 @@ import { getWorkflowExecutionMetadata, setAllWorkflowExecutionMetadata, setWorkflowExecutionMetadata, -} from './WorkflowExecutionMetadata'; +} from './ExecutionMetadata'; import { getSecretsProxy } from './Secrets'; import Container from 'typedi'; import type { BinaryData } from './BinaryData/types'; @@ -808,7 +807,7 @@ export async function proxyRequestToAxios( response: pick(response, ['headers', 'status', 'statusText']), }); } else if ('rejectUnauthorized' in configObject && error.code?.includes('CERT')) { - throw new NodeSSLError(error); + throw new NodeSslError(error); } } @@ -3442,7 +3441,7 @@ export function getExecuteFunctions( addInputData( connectionType: ConnectionTypes, - data: INodeExecutionData[][] | ExecutionError, + data: INodeExecutionData[][] | ExecutionBaseError, ): { index: number } { const nodeName = this.getNode().name; let currentNodeRunIndex = 0; @@ -3473,7 +3472,7 @@ export function getExecuteFunctions( addOutputData( connectionType: ConnectionTypes, currentNodeRunIndex: number, - data: INodeExecutionData[][] | ExecutionError, + data: INodeExecutionData[][] | ExecutionBaseError, ): void { addExecutionDataFunctions( 'output', diff --git a/packages/core/src/WorkflowExecute.ts b/packages/core/src/WorkflowExecute.ts index 0822848f522c4..0da482ae552e5 100644 --- a/packages/core/src/WorkflowExecute.ts +++ b/packages/core/src/WorkflowExecute.ts @@ -6,7 +6,7 @@ import { setMaxListeners } from 'events'; import PCancelable from 'p-cancelable'; import type { - ExecutionError, + ExecutionBaseError, ExecutionStatus, GenericValue, IConnection, @@ -797,7 +797,7 @@ export class WorkflowExecute { // Variables which hold temporary data for each node-execution let executionData: IExecuteData; - let executionError: ExecutionError | undefined; + let executionError: ExecutionBaseError | undefined; let executionNode: INode; let nodeSuccessData: INodeExecutionData[][] | null | undefined; let runIndex: number; @@ -838,11 +838,13 @@ export class WorkflowExecute { await this.executeHook('workflowExecuteBefore', [workflow]); } } catch (error) { + const e = error as unknown as ExecutionBaseError; + // Set the error that it can be saved correctly executionError = { - ...(error as NodeOperationError | NodeApiError), - message: (error as NodeOperationError | NodeApiError).message, - stack: (error as NodeOperationError | NodeApiError).stack, + ...e, + message: e.message, + stack: e.stack, }; // Set the incoming data of the node that it can be saved correctly @@ -1253,10 +1255,12 @@ export class WorkflowExecute { } catch (error) { this.runExecutionData.resultData.lastNodeExecuted = executionData.node.name; + const e = error as unknown as ExecutionBaseError; + executionError = { - ...(error as NodeOperationError | NodeApiError), - message: (error as NodeOperationError | NodeApiError).message, - stack: (error as NodeOperationError | NodeApiError).stack, + ...e, + message: e.message, + stack: e.stack, }; Logger.debug(`Running node "${executionNode.name}" finished with error`, { @@ -1325,11 +1329,17 @@ export class WorkflowExecute { lineResult.json.$error !== undefined && lineResult.json.$json !== undefined ) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore lineResult.error = lineResult.json.$error as NodeApiError | NodeOperationError; lineResult.json = { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore error: (lineResult.json.$error as NodeApiError | NodeOperationError).message, }; } else if (lineResult.error !== undefined) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore lineResult.json = { error: lineResult.error.message }; } } @@ -1697,7 +1707,7 @@ export class WorkflowExecute { async processSuccessExecution( startedAt: Date, workflow: Workflow, - executionError?: ExecutionError, + executionError?: ExecutionBaseError, closeFunction?: Promise, ): Promise { const fullRunData = this.getFullRunData(startedAt); @@ -1711,7 +1721,7 @@ export class WorkflowExecute { ...executionError, message: executionError.message, stack: executionError.stack, - } as ExecutionError; + } as ExecutionBaseError; if (executionError.message?.includes('canceled')) { fullRunData.status = 'canceled'; } diff --git a/packages/core/src/errors/abstract/binary-data.error.ts b/packages/core/src/errors/abstract/binary-data.error.ts new file mode 100644 index 0000000000000..061e95baf4c2c --- /dev/null +++ b/packages/core/src/errors/abstract/binary-data.error.ts @@ -0,0 +1,3 @@ +import { ApplicationError } from 'n8n-workflow'; + +export abstract class BinaryDataError extends ApplicationError {} diff --git a/packages/core/src/errors/abstract/filesystem.error.ts b/packages/core/src/errors/abstract/filesystem.error.ts new file mode 100644 index 0000000000000..5ee937af681b3 --- /dev/null +++ b/packages/core/src/errors/abstract/filesystem.error.ts @@ -0,0 +1,7 @@ +import { ApplicationError } from 'n8n-workflow'; + +export abstract class FileSystemError extends ApplicationError { + constructor(message: string, filePath: string) { + super(message, { extra: { filePath } }); + } +} diff --git a/packages/core/src/errors/disallowed-filepath.error.ts b/packages/core/src/errors/disallowed-filepath.error.ts new file mode 100644 index 0000000000000..4d6ac5684d28b --- /dev/null +++ b/packages/core/src/errors/disallowed-filepath.error.ts @@ -0,0 +1,7 @@ +import { FileSystemError } from './abstract/filesystem.error'; + +export class DisallowedFilepathError extends FileSystemError { + constructor(filePath: string) { + super('Disallowed path detected', filePath); + } +} diff --git a/packages/core/src/errors/file-not-found.error.ts b/packages/core/src/errors/file-not-found.error.ts new file mode 100644 index 0000000000000..208bb0d354ec8 --- /dev/null +++ b/packages/core/src/errors/file-not-found.error.ts @@ -0,0 +1,7 @@ +import { FileSystemError } from './abstract/filesystem.error'; + +export class FileNotFoundError extends FileSystemError { + constructor(filePath: string) { + super('File not found', filePath); + } +} diff --git a/packages/core/src/errors/filesystem.errors.ts b/packages/core/src/errors/filesystem.errors.ts deleted file mode 100644 index f7928a333fb2a..0000000000000 --- a/packages/core/src/errors/filesystem.errors.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { ReportableError } from 'n8n-workflow'; - -abstract class FileSystemError extends ReportableError { - constructor(message: string, filePath: string) { - super(message, { extra: { filePath } }); - } -} - -export class FileNotFoundError extends FileSystemError { - constructor(filePath: string) { - super('File not found', filePath); - } -} - -export class BinaryFileNotFoundError extends FileNotFoundError {} - -export class InvalidPathError extends FileSystemError { - constructor(filePath: string) { - super('Invalid path detected', filePath); - } -} diff --git a/packages/core/src/errors/index.ts b/packages/core/src/errors/index.ts index ef2f8401c2186..1370daf3b3f01 100644 --- a/packages/core/src/errors/index.ts +++ b/packages/core/src/errors/index.ts @@ -1 +1,5 @@ -export { BinaryFileNotFoundError, FileNotFoundError, InvalidPathError } from './filesystem.errors'; +export { FileNotFoundError } from './file-not-found.error'; +export { DisallowedFilepathError } from './disallowed-filepath.error'; +export { InvalidModeError } from './invalid-mode.error'; +export { InvalidManagerError } from './invalid-manager.error'; +export { InvalidExecutionMetadata } from './invalid-execution-metadata.error'; diff --git a/packages/core/src/errors/invalid-execution-metadata.error.ts b/packages/core/src/errors/invalid-execution-metadata.error.ts new file mode 100644 index 0000000000000..3dde839525a48 --- /dev/null +++ b/packages/core/src/errors/invalid-execution-metadata.error.ts @@ -0,0 +1,13 @@ +import { ApplicationError } from 'n8n-workflow'; + +export class InvalidExecutionMetadata extends ApplicationError { + constructor( + public type: 'key' | 'value', + key: unknown, + message?: string, + options?: ErrorOptions, + ) { + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + super(message ?? `Custom data ${type}s must be a string (key "${key}")`, options); + } +} diff --git a/packages/core/src/errors/invalid-manager.error.ts b/packages/core/src/errors/invalid-manager.error.ts new file mode 100644 index 0000000000000..ecd1f75ca3fa2 --- /dev/null +++ b/packages/core/src/errors/invalid-manager.error.ts @@ -0,0 +1,7 @@ +import { BinaryDataError } from './abstract/binary-data.error'; + +export class InvalidManagerError extends BinaryDataError { + constructor(mode: string) { + super(`No binary data manager found for: ${mode}`); + } +} diff --git a/packages/core/src/errors/invalid-mode.error.ts b/packages/core/src/errors/invalid-mode.error.ts new file mode 100644 index 0000000000000..348fbb410dbca --- /dev/null +++ b/packages/core/src/errors/invalid-mode.error.ts @@ -0,0 +1,8 @@ +import { ApplicationError } from 'n8n-workflow'; +import { CONFIG_MODES } from '../BinaryData/utils'; + +export class InvalidModeError extends ApplicationError { + constructor() { + super(`Invalid binary data mode. Valid modes: ${CONFIG_MODES.join(', ')}`); + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b934894f4d9e2..3a988ecc4da92 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -18,3 +18,4 @@ export * from './errors'; export { ObjectStoreService } from './ObjectStore/ObjectStore.service.ee'; export { BinaryData } from './BinaryData/types'; export { isStoredMode as isValidNonDefaultMode } from './BinaryData/utils'; +export * from './ExecutionMetadata'; diff --git a/packages/core/test/WorkflowExecutionMetadata.test.ts b/packages/core/test/WorkflowExecutionMetadata.test.ts index 8af21444b43cb..ce6290d421903 100644 --- a/packages/core/test/WorkflowExecutionMetadata.test.ts +++ b/packages/core/test/WorkflowExecutionMetadata.test.ts @@ -4,8 +4,8 @@ import { KV_LIMIT, setAllWorkflowExecutionMetadata, setWorkflowExecutionMetadata, - ExecutionMetadataValidationError, -} from '@/WorkflowExecutionMetadata'; + InvalidExecutionMetadata, +} from '@/errors/invalid-execution-metadata.error'; import type { IRunExecutionData } from 'n8n-workflow'; describe('Execution Metadata functions', () => { @@ -52,7 +52,7 @@ describe('Execution Metadata functions', () => { } as IRunExecutionData; expect(() => setWorkflowExecutionMetadata(executionData, 'test1', 1234)).not.toThrow( - ExecutionMetadataValidationError, + InvalidExecutionMetadata, ); expect(metadata).toEqual({ @@ -60,7 +60,7 @@ describe('Execution Metadata functions', () => { }); expect(() => setWorkflowExecutionMetadata(executionData, 'test2', {})).toThrow( - ExecutionMetadataValidationError, + InvalidExecutionMetadata, ); expect(metadata).not.toEqual({ @@ -84,7 +84,7 @@ describe('Execution Metadata functions', () => { test3: 'value3', test4: 'value4', }), - ).toThrow(ExecutionMetadataValidationError); + ).toThrow(InvalidExecutionMetadata); expect(metadata).toEqual({ test3: 'value3', @@ -101,7 +101,7 @@ describe('Execution Metadata functions', () => { } as IRunExecutionData; expect(() => setWorkflowExecutionMetadata(executionData, 'te$t1$', 1234)).toThrow( - ExecutionMetadataValidationError, + InvalidExecutionMetadata, ); expect(metadata).not.toEqual({ diff --git a/packages/workflow/src/ErrorReporterProxy.ts b/packages/workflow/src/ErrorReporterProxy.ts index 092060e40ad78..460e2def85076 100644 --- a/packages/workflow/src/ErrorReporterProxy.ts +++ b/packages/workflow/src/ErrorReporterProxy.ts @@ -1,5 +1,5 @@ import * as Logger from './LoggerProxy'; -import { ReportableError, type ReportingOptions } from './errors/reportable.error'; +import { ApplicationError, type ReportingOptions } from './errors/application.error'; interface ErrorReporter { report: (error: Error | string, options?: ReportingOptions) => void; @@ -10,7 +10,7 @@ const instance: ErrorReporter = { if (error instanceof Error) { let e = error; do { - const meta = e instanceof ReportableError ? e.extra : undefined; + const meta = e instanceof ApplicationError ? e.extra : undefined; Logger.error(`${e.constructor.name}: ${e.message}`, meta); e = e.cause as Error; } while (e); diff --git a/packages/workflow/src/Expression.ts b/packages/workflow/src/Expression.ts index e5f99ab13744f..2511085b77c5b 100644 --- a/packages/workflow/src/Expression.ts +++ b/packages/workflow/src/Expression.ts @@ -15,7 +15,8 @@ import type { NodeParameterValueType, WorkflowExecuteMode, } from './Interfaces'; -import { ExpressionError, ExpressionExtensionError } from './ExpressionError'; +import { ExpressionError } from './errors/expression.error'; +import { ExpressionExtensionError } from './errors/expression-extension.error'; import { WorkflowDataProxy } from './WorkflowDataProxy'; import type { Workflow } from './Workflow'; diff --git a/packages/workflow/src/Extensions/ArrayExtensions.ts b/packages/workflow/src/Extensions/ArrayExtensions.ts index 76fcef22066bc..b414014d0f7f8 100644 --- a/packages/workflow/src/Extensions/ArrayExtensions.ts +++ b/packages/workflow/src/Extensions/ArrayExtensions.ts @@ -1,4 +1,5 @@ -import { ExpressionError, ExpressionExtensionError } from '../ExpressionError'; +import { ExpressionError } from '../errors/expression.error'; +import { ExpressionExtensionError } from '../errors/expression-extension.error'; import type { ExtensionMap } from './Extensions'; import { compact as oCompact } from './ObjectExtensions'; import deepEqual from 'deep-equal'; diff --git a/packages/workflow/src/Extensions/DateExtensions.ts b/packages/workflow/src/Extensions/DateExtensions.ts index 8336ba5df38e4..c99dec4706cda 100644 --- a/packages/workflow/src/Extensions/DateExtensions.ts +++ b/packages/workflow/src/Extensions/DateExtensions.ts @@ -1,4 +1,4 @@ -import { ExpressionExtensionError } from './../ExpressionError'; +import { ExpressionExtensionError } from '../errors/expression-extension.error'; import { DateTime } from 'luxon'; import type { diff --git a/packages/workflow/src/Extensions/ExpressionExtension.ts b/packages/workflow/src/Extensions/ExpressionExtension.ts index 809d7a488682d..29a9bbee1f731 100644 --- a/packages/workflow/src/Extensions/ExpressionExtension.ts +++ b/packages/workflow/src/Extensions/ExpressionExtension.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { DateTime } from 'luxon'; -import { ExpressionExtensionError } from '../ExpressionError'; +import { ExpressionExtensionError } from '../errors/expression-extension.error'; import { parse, visit, types, print } from 'recast'; import { getOption } from 'recast/lib/util'; import type { Config as EsprimaConfig } from 'esprima-next'; diff --git a/packages/workflow/src/Extensions/ExtendedFunctions.ts b/packages/workflow/src/Extensions/ExtendedFunctions.ts index cf3e480348c9b..091cdc8b0c67c 100644 --- a/packages/workflow/src/Extensions/ExtendedFunctions.ts +++ b/packages/workflow/src/Extensions/ExtendedFunctions.ts @@ -1,4 +1,5 @@ -import { ExpressionError, ExpressionExtensionError } from '../ExpressionError'; +import { ExpressionError } from '../errors/expression.error'; +import { ExpressionExtensionError } from '../errors/expression-extension.error'; import { average as aAverage } from './ArrayExtensions'; const min = Math.min; diff --git a/packages/workflow/src/Extensions/NumberExtensions.ts b/packages/workflow/src/Extensions/NumberExtensions.ts index b7ac6814a9e1b..48d019331ec16 100644 --- a/packages/workflow/src/Extensions/NumberExtensions.ts +++ b/packages/workflow/src/Extensions/NumberExtensions.ts @@ -1,7 +1,7 @@ /** * @jest-environment jsdom */ -import { ExpressionExtensionError } from './../ExpressionError'; +import { ExpressionExtensionError } from '../errors/expression-extension.error'; import type { ExtensionMap } from './Extensions'; function format(value: number, extraArgs: unknown[]): string { diff --git a/packages/workflow/src/Extensions/ObjectExtensions.ts b/packages/workflow/src/Extensions/ObjectExtensions.ts index dcc319a179abf..61d23d3363ecb 100644 --- a/packages/workflow/src/Extensions/ObjectExtensions.ts +++ b/packages/workflow/src/Extensions/ObjectExtensions.ts @@ -1,4 +1,4 @@ -import { ExpressionExtensionError } from '../ExpressionError'; +import { ExpressionExtensionError } from '../errors/expression-extension.error'; import type { ExtensionMap } from './Extensions'; function isEmpty(value: object): boolean { diff --git a/packages/workflow/src/Extensions/StringExtensions.ts b/packages/workflow/src/Extensions/StringExtensions.ts index b6d40580b9da1..da991af75c580 100644 --- a/packages/workflow/src/Extensions/StringExtensions.ts +++ b/packages/workflow/src/Extensions/StringExtensions.ts @@ -1,10 +1,10 @@ // import { createHash } from 'crypto'; import { titleCase } from 'title-case'; -import * as ExpressionError from '../ExpressionError'; import type { ExtensionMap } from './Extensions'; import CryptoJS from 'crypto-js'; import { encode } from 'js-base64'; import { transliterate } from 'transliteration'; +import { ExpressionExtensionError } from '../errors/expression-extension.error'; const hashFunctions: Record = { md5: CryptoJS.MD5, @@ -122,7 +122,7 @@ function hash(value: string, extraArgs?: unknown): string { } const hashFunction = hashFunctions[algorithm.toLowerCase()]; if (!hashFunction) { - throw new ExpressionError.ExpressionExtensionError( + throw new ExpressionExtensionError( `Unknown algorithm ${algorithm}. Available algorithms are: ${Object.keys(hashFunctions) .map((s) => s.toUpperCase()) .join(', ')}, and Base64.`, @@ -194,7 +194,7 @@ function toDate(value: string): Date { const date = new Date(Date.parse(value)); if (date.toString() === 'Invalid Date') { - throw new ExpressionError.ExpressionExtensionError('cannot convert to date'); + throw new ExpressionExtensionError('cannot convert to date'); } // If time component is not specified, force 00:00h if (!/:/.test(value)) { @@ -224,7 +224,7 @@ function toInt(value: string, extraArgs: Array) { const int = parseInt(value.replace(CURRENCY_REGEXP, ''), radix); if (isNaN(int)) { - throw new ExpressionError.ExpressionExtensionError('cannot convert to integer'); + throw new ExpressionExtensionError('cannot convert to integer'); } return int; @@ -232,15 +232,13 @@ function toInt(value: string, extraArgs: Array) { function toFloat(value: string) { if (value.includes(',')) { - throw new ExpressionError.ExpressionExtensionError( - 'cannot convert to float, expected . as decimal separator', - ); + throw new ExpressionExtensionError('cannot convert to float, expected . as decimal separator'); } const float = parseFloat(value.replace(CURRENCY_REGEXP, '')); if (isNaN(float)) { - throw new ExpressionError.ExpressionExtensionError('cannot convert to float'); + throw new ExpressionExtensionError('cannot convert to float'); } return float; diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index bc879ab5256b5..7ca860d67ca30 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -13,12 +13,13 @@ import type { AuthenticationMethod } from './Authentication'; import type { CODE_EXECUTION_MODES, CODE_LANGUAGES, LOG_LEVELS } from './Constants'; import type { IDeferredPromise } from './DeferredPromise'; import type { ExecutionStatus } from './ExecutionStatus'; -import type { ExpressionError } from './ExpressionError'; -import type { NodeApiError, NodeOperationError } from './NodeErrors'; +import type { ExpressionError } from './errors/expression.error'; import type { Workflow } from './Workflow'; -import type { WorkflowActivationError } from './WorkflowActivationError'; -import type { WorkflowOperationError } from './WorkflowErrors'; +import type { WorkflowActivationError } from './errors/workflow-activation.error'; +import type { WorkflowOperationError } from './errors/workflow-operation.error'; import type { WorkflowHooks } from './WorkflowHooks'; +import type { NodeOperationError } from './errors/node-operation.error'; +import type { NodeApiError } from './errors/node-api.error'; export interface IAdditionalCredentialOptions { oauth2?: IOAuth2Options; @@ -2373,3 +2374,5 @@ export type BannerName = | 'TRIAL' | 'NON_PRODUCTION_LICENSE' | 'EMAIL_CONFIRMATION'; + +export type Severity = 'warning' | 'error'; diff --git a/packages/workflow/src/NodeErrors.ts b/packages/workflow/src/NodeErrors.ts deleted file mode 100644 index 9ad7ca1d5faca..0000000000000 --- a/packages/workflow/src/NodeErrors.ts +++ /dev/null @@ -1,512 +0,0 @@ -/* eslint-disable @typescript-eslint/naming-convention */ - -import { parseString } from 'xml2js'; -import { removeCircularRefs, isTraversableObject } from './utils'; -import type { IDataObject, INode, IStatusCodeMessages, JsonObject } from './Interfaces'; - -/** - * Top-level properties where an error message can be found in an API response. - */ -const ERROR_MESSAGE_PROPERTIES = [ - 'cause', - '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']; - -/** - * Descriptive messages for common errors. - */ -const COMMON_ERRORS: IDataObject = { - // nodeJS errors - ECONNREFUSED: 'The service refused the connection - perhaps it is offline', - ECONNRESET: - 'The connection to the server wes closed unexpectedly, perhaps it is offline. You can retry request immidiately or wait and retry later.', - ENOTFOUND: - 'The connection cannot be established, this usually occurs due to an incorrect host(domain) value', - ETIMEDOUT: - "The connection timed out, consider setting 'Retry on Fail' option in the node settings", - ERRADDRINUSE: - 'The port is already occupied by some other application, if possible change the port or kill the application that is using it', - EADDRNOTAVAIL: 'The address is not available, ensure that you have the right IP address', - ECONNABORTED: 'The connection was aborted, perhaps the server is offline', - EHOSTUNREACH: 'The host is unreachable, perhaps the server is offline', - EAI_AGAIN: 'The DNS server returned an error, perhaps the server is offline', - ENOENT: 'The file or directory does not exist', - EISDIR: 'The file path expected but a given path is a directory', - ENOTDIR: 'The directory path expected but a given path is a file', - EACCES: 'Forbidden by access permissions, make sure you have the right permissions', - EEXIST: 'The file or directory already exists', - EPERM: 'Operation not permitted, make sure you have the right permissions', - // other errors - GETADDRINFO: 'The server closed the connection unexpectedly', -}; - -/** - * Descriptive messages for common HTTP status codes - * this is used by NodeApiError class - */ -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', - - '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 - try again later or consider setting this node to retry automatically (in the node settings)', - '504': 'Gateway timed out - perhaps try again later?', -}; - -const UNKNOWN_ERROR_MESSAGE = 'UNKNOWN ERROR - check the detailed error for more information'; -const UNKNOWN_ERROR_MESSAGE_CRED = 'UNKNOWN ERROR'; - -export type Severity = 'warning' | 'error'; - -interface ExecutionBaseErrorOptions { - cause?: Error | JsonObject; -} - -interface NodeOperationErrorOptions { - message?: string; - description?: string; - runIndex?: number; - itemIndex?: number; - severity?: Severity; - messageMapping?: { [key: string]: string }; // allows to pass custom mapping for error messages scoped to a node -} - -interface NodeApiErrorOptions extends NodeOperationErrorOptions { - message?: string; - httpCode?: string; - parseXml?: boolean; -} - -export abstract class ExecutionBaseError extends Error { - description: string | null | undefined; - - cause: Error | JsonObject | undefined; - - timestamp: number; - - context: IDataObject = {}; - - lineNumber: number | undefined; - - severity: Severity = 'error'; - - constructor(message: string, { cause }: ExecutionBaseErrorOptions) { - const options = cause instanceof Error ? { cause } : {}; - super(message, options); - - this.name = this.constructor.name; - this.timestamp = Date.now(); - - if (cause instanceof ExecutionBaseError) { - this.context = cause.context; - } else if (cause && !(cause instanceof Error)) { - this.cause = cause; - } - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - toJSON?(): any { - return { - message: this.message, - lineNumber: this.lineNumber, - timestamp: this.timestamp, - name: this.name, - description: this.description, - context: this.context, - cause: this.cause, - }; - } -} - -/** - * Base class for specific NodeError-types, with functionality for finding - * a value recursively inside an error object. - */ -export abstract class NodeError extends ExecutionBaseError { - node: INode; - - constructor(node: INode, error: Error | JsonObject) { - const message = error instanceof Error ? error.message : ''; - super(message, { cause: error }); - this.node = node; - } - - /** - * 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. - * - */ - protected findProperty( - jsonError: JsonObject, - potentialKeys: string[], - traversalKeys: string[] = [], - ): string | null { - for (const key of potentialKeys) { - const value = jsonError[key]; - if (value) { - if (typeof value === 'string') return value; - if (typeof value === 'number') return value.toString(); - if (Array.isArray(value)) { - const resolvedErrors: string[] = value - // eslint-disable-next-line @typescript-eslint/no-shadow - .map((jsonError) => { - if (typeof jsonError === 'string') return jsonError; - if (typeof jsonError === 'number') return jsonError.toString(); - if (isTraversableObject(jsonError)) { - return this.findProperty(jsonError, potentialKeys); - } - return null; - }) - .filter((errorValue): errorValue is string => errorValue !== null); - - if (resolvedErrors.length === 0) { - return null; - } - return resolvedErrors.join(' | '); - } - if (isTraversableObject(value)) { - const property = this.findProperty(value, potentialKeys); - if (property) { - return property; - } - } - } - } - - for (const key of traversalKeys) { - const value = jsonError[key]; - if (isTraversableObject(value)) { - const property = this.findProperty(value, potentialKeys, traversalKeys); - if (property) { - return property; - } - } - } - - return null; - } - - /** - * Set descriptive error message if code is provided or if message contains any of the common errors, - * update description to include original message plus the description - */ - protected setDescriptiveErrorMessage( - message: string, - description: string | undefined | null, - code?: string | null, - messageMapping?: { [key: string]: string }, - ) { - let newMessage = message; - let newDescription = description as string; - - if (messageMapping) { - for (const [mapKey, mapMessage] of Object.entries(messageMapping)) { - if ((message || '').toUpperCase().includes(mapKey.toUpperCase())) { - newMessage = mapMessage; - newDescription = this.updateDescription(message, description); - break; - } - } - if (newMessage !== message) { - return [newMessage, newDescription]; - } - } - - // if code is provided and it is in the list of common errors set the message and return early - if (code && COMMON_ERRORS[code.toUpperCase()]) { - newMessage = COMMON_ERRORS[code] as string; - newDescription = this.updateDescription(message, description); - return [newMessage, newDescription]; - } - - // check if message contains any of the common errors and set the message and description - for (const [errorCode, errorDescriptiveMessage] of Object.entries(COMMON_ERRORS)) { - if ((message || '').toUpperCase().includes(errorCode.toUpperCase())) { - newMessage = errorDescriptiveMessage as string; - newDescription = this.updateDescription(message, description); - break; - } - } - - return [newMessage, newDescription]; - } - - protected updateDescription(message: string, description: string | undefined | null) { - return `${message}${description ? ` - ${description}` : ''}`; - } -} - -/** - * Class for instantiating an operational error, e.g. an invalid credentials error. - */ -export class NodeOperationError extends NodeError { - lineNumber: number | undefined; - - constructor(node: INode, error: Error | string, options: NodeOperationErrorOptions = {}) { - if (typeof error === 'string') { - error = new Error(error); - } - super(node, error); - - if (options.message) this.message = options.message; - if (options.severity) this.severity = options.severity; - this.description = options.description; - this.context.runIndex = options.runIndex; - this.context.itemIndex = options.itemIndex; - - if (this.message === this.description) { - this.description = undefined; - } - - [this.message, this.description] = this.setDescriptiveErrorMessage( - this.message, - this.description, - undefined, - options.messageMapping, - ); - } -} - -/** - * 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, - runIndex, - itemIndex, - severity, - messageMapping, - }: NodeApiErrorOptions = {}, - ) { - super(node, error); - - // only for request library error - if (error.error) { - removeCircularRefs(error.error as JsonObject); - } - - // if not description provided, try to find it in the error object - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (!description && (error.description || (error?.reason as IDataObject)?.description)) { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - this.description = (error.description || - (error?.reason as IDataObject)?.description) as string; - } - - // if not message provided, try to find it in the error object or set description as message - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - if (!message && (error.message || (error?.reason as IDataObject)?.message || description)) { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - this.message = (error.message || - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - (error?.reason as IDataObject)?.message || - description) as string; - } - - // if it's an error generated by axios - // look for descriptions in the response object - if (error.reason) { - const reason: IDataObject = error.reason as unknown as IDataObject; - - if (reason.isAxiosError && reason.response) { - error = reason.response as JsonObject; - } - } - - // set http code of this error - if (httpCode) { - this.httpCode = httpCode; - } else { - this.httpCode = - this.findProperty(error, ERROR_STATUS_PROPERTIES, ERROR_NESTING_PROPERTIES) ?? null; - } - - if (severity) { - this.severity = severity; - } else if (this.httpCode?.charAt(0) !== '5') { - this.severity = 'warning'; - } - - // set description of this error - if (description) { - this.description = description; - } - - if (!this.description) { - if (parseXml) { - this.setDescriptionFromXml(error.error as string); - } else { - this.description = this.findProperty( - error, - ERROR_MESSAGE_PROPERTIES, - ERROR_NESTING_PROPERTIES, - ); - } - } - - // set message if provided or set default message based on http code - if (message) { - this.message = message; - } else { - this.setDefaultStatusCodeMessage(); - } - - // if message and description are the same, unset redundant description - if (this.message === this.description) { - this.description = undefined; - } - - // if message contain common error code set descriptive message and update description - [this.message, this.description] = this.setDescriptiveErrorMessage( - this.message, - this.description, - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - this.httpCode || - (error?.code as string) || - ((error?.reason as JsonObject)?.code as string) || - undefined, - messageMapping, - ); - - if (runIndex !== undefined) this.context.runIndex = runIndex; - if (itemIndex !== undefined) this.context.itemIndex = itemIndex; - } - - private setDescriptionFromXml(xml: string) { - parseString(xml, { explicitArray: false }, (_, result) => { - if (!result) return; - - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - const topLevelKey = Object.keys(result)[0]; - this.description = this.findProperty( - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access - result[topLevelKey], - ERROR_MESSAGE_PROPERTIES, - ['Error'].concat(ERROR_NESTING_PROPERTIES), - ); - }); - } - - /** - * Set the error's message based on the HTTP status code. - */ - private setDefaultStatusCodeMessage() { - // Set generic error message for 502 Bad Gateway - if (!this.httpCode && this.message && this.message.toLowerCase().includes('bad gateway')) { - this.httpCode = '502'; - } - - if (!this.httpCode) { - this.httpCode = null; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - this.message = this.message || this.description || UNKNOWN_ERROR_MESSAGE; - return; - } - - if (STATUS_CODE_MESSAGES[this.httpCode]) { - this.description = this.updateDescription(this.message, this.description); - this.message = STATUS_CODE_MESSAGES[this.httpCode]; - return; - } - - switch (this.httpCode.charAt(0)) { - case '4': - this.description = this.updateDescription(this.message, this.description); - this.message = STATUS_CODE_MESSAGES['4XX']; - break; - case '5': - this.description = this.updateDescription(this.message, this.description); - this.message = STATUS_CODE_MESSAGES['5XX']; - break; - default: - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - this.message = this.message || this.description || UNKNOWN_ERROR_MESSAGE; - } - if (this.node.type === 'n8n-nodes-base.noOp' && this.message === UNKNOWN_ERROR_MESSAGE) { - this.message = `${UNKNOWN_ERROR_MESSAGE_CRED} - ${this.httpCode}`; - } - } -} - -export class NodeSSLError extends ExecutionBaseError { - constructor(cause: Error) { - super("SSL Issue: consider using the 'Ignore SSL issues' option", { cause }); - } -} diff --git a/packages/workflow/src/RoutingNode.ts b/packages/workflow/src/RoutingNode.ts index 18504f3ff18ab..fcd8fea8c384b 100644 --- a/packages/workflow/src/RoutingNode.ts +++ b/packages/workflow/src/RoutingNode.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/prefer-nullish-coalescing */ @@ -37,11 +38,14 @@ import type { PostReceiveAction, JsonObject, } from './Interfaces'; -import type { NodeError } from './NodeErrors'; -import { NodeApiError, NodeOperationError } from './NodeErrors'; + import * as NodeHelpers from './NodeHelpers'; import type { Workflow } from './Workflow'; +import type { NodeError } from './errors/abstract/node.error'; + +import { NodeOperationError } from './errors/node-operation.error'; +import { NodeApiError } from './errors/node-api.error'; export class RoutingNode { additionalData: IWorkflowExecuteAdditionalData; diff --git a/packages/workflow/src/WorkflowDataProxy.ts b/packages/workflow/src/WorkflowDataProxy.ts index d1c657088523c..3ead091171539 100644 --- a/packages/workflow/src/WorkflowDataProxy.ts +++ b/packages/workflow/src/WorkflowDataProxy.ts @@ -24,7 +24,7 @@ import type { ProxyInput, } from './Interfaces'; import * as NodeHelpers from './NodeHelpers'; -import { ExpressionError } from './ExpressionError'; +import { ExpressionError } from './errors/expression.error'; import type { Workflow } from './Workflow'; import { augmentArray, augmentObject } from './AugmentObject'; import { deepCopy } from './utils'; diff --git a/packages/workflow/src/WorkflowErrors.ts b/packages/workflow/src/WorkflowErrors.ts deleted file mode 100644 index 662dfda1cb26e..0000000000000 --- a/packages/workflow/src/WorkflowErrors.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { INode } from './Interfaces'; -import { ExecutionBaseError } from './NodeErrors'; - -/** - * Class for instantiating an operational error, e.g. a timeout error. - */ -export class WorkflowOperationError extends ExecutionBaseError { - node: INode | undefined; - - timestamp: number; - - lineNumber: number | undefined; - - description: string | undefined; - - constructor(message: string, node?: INode) { - super(message, { cause: undefined }); - this.severity = 'warning'; - this.name = this.constructor.name; - this.node = node; - this.timestamp = Date.now(); - } -} - -export class SubworkflowOperationError extends WorkflowOperationError { - description = ''; - - cause: { message: string; stack: string }; - - constructor(message: string, description: string) { - super(message); - this.name = this.constructor.name; - this.description = description; - - this.cause = { - message, - stack: this.stack as string, - }; - } -} - -export class CliWorkflowOperationError extends SubworkflowOperationError {} diff --git a/packages/workflow/src/errors/abstract/execution-base.error.ts b/packages/workflow/src/errors/abstract/execution-base.error.ts new file mode 100644 index 0000000000000..c1de0ab54fe4e --- /dev/null +++ b/packages/workflow/src/errors/abstract/execution-base.error.ts @@ -0,0 +1,47 @@ +import type { IDataObject, JsonObject, Severity } from '../../Interfaces'; +import { ApplicationError } from '../application.error'; + +interface ExecutionBaseErrorOptions { + cause?: Error | JsonObject; +} + +export abstract class ExecutionBaseError extends ApplicationError { + description: string | null | undefined; + + cause: Error | JsonObject | undefined; + + timestamp: number; + + context: IDataObject = {}; + + lineNumber: number | undefined; + + severity: Severity = 'error'; + + constructor(message: string, { cause }: ExecutionBaseErrorOptions) { + const options = cause instanceof Error ? { cause } : {}; + super(message, options); + + this.name = this.constructor.name; + this.timestamp = Date.now(); + + if (cause instanceof ExecutionBaseError) { + this.context = cause.context; + } else if (cause && !(cause instanceof Error)) { + this.cause = cause; + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + toJSON?(): any { + return { + message: this.message, + lineNumber: this.lineNumber, + timestamp: this.timestamp, + name: this.name, + description: this.description, + context: this.context, + cause: this.cause, + }; + } +} diff --git a/packages/workflow/src/errors/abstract/node.error.ts b/packages/workflow/src/errors/abstract/node.error.ts new file mode 100644 index 0000000000000..4ef3e3d201e38 --- /dev/null +++ b/packages/workflow/src/errors/abstract/node.error.ts @@ -0,0 +1,168 @@ +import { isTraversableObject } from '../../utils'; +import type { IDataObject, INode, JsonObject } from '../..'; +import { ExecutionBaseError } from './execution-base.error'; + +/** + * Descriptive messages for common errors. + */ +const COMMON_ERRORS: IDataObject = { + // nodeJS errors + ECONNREFUSED: 'The service refused the connection - perhaps it is offline', + ECONNRESET: + 'The connection to the server wes closed unexpectedly, perhaps it is offline. You can retry request immidiately or wait and retry later.', + ENOTFOUND: + 'The connection cannot be established, this usually occurs due to an incorrect host(domain) value', + ETIMEDOUT: + "The connection timed out, consider setting 'Retry on Fail' option in the node settings", + ERRADDRINUSE: + 'The port is already occupied by some other application, if possible change the port or kill the application that is using it', + EADDRNOTAVAIL: 'The address is not available, ensure that you have the right IP address', + ECONNABORTED: 'The connection was aborted, perhaps the server is offline', + EHOSTUNREACH: 'The host is unreachable, perhaps the server is offline', + EAI_AGAIN: 'The DNS server returned an error, perhaps the server is offline', + ENOENT: 'The file or directory does not exist', + EISDIR: 'The file path expected but a given path is a directory', + ENOTDIR: 'The directory path expected but a given path is a file', + EACCES: 'Forbidden by access permissions, make sure you have the right permissions', + EEXIST: 'The file or directory already exists', + EPERM: 'Operation not permitted, make sure you have the right permissions', + // other errors + GETADDRINFO: 'The server closed the connection unexpectedly', +}; + +/** + * Base class for specific NodeError-types, with functionality for finding + * a value recursively inside an error object. + */ +export abstract class NodeError extends ExecutionBaseError { + node: INode; + + constructor(node: INode, error: Error | JsonObject) { + const message = error instanceof Error ? error.message : ''; + super(message, { cause: error }); + this.node = node; + } + + /** + * 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. + * + */ + protected findProperty( + jsonError: JsonObject, + potentialKeys: string[], + traversalKeys: string[] = [], + ): string | null { + for (const key of potentialKeys) { + const value = jsonError[key]; + if (value) { + if (typeof value === 'string') return value; + if (typeof value === 'number') return value.toString(); + if (Array.isArray(value)) { + const resolvedErrors: string[] = value + // eslint-disable-next-line @typescript-eslint/no-shadow + .map((jsonError) => { + if (typeof jsonError === 'string') return jsonError; + if (typeof jsonError === 'number') return jsonError.toString(); + if (isTraversableObject(jsonError)) { + return this.findProperty(jsonError, potentialKeys); + } + return null; + }) + .filter((errorValue): errorValue is string => errorValue !== null); + + if (resolvedErrors.length === 0) { + return null; + } + return resolvedErrors.join(' | '); + } + if (isTraversableObject(value)) { + const property = this.findProperty(value, potentialKeys); + if (property) { + return property; + } + } + } + } + + for (const key of traversalKeys) { + const value = jsonError[key]; + if (isTraversableObject(value)) { + const property = this.findProperty(value, potentialKeys, traversalKeys); + if (property) { + return property; + } + } + } + + return null; + } + + /** + * Set descriptive error message if code is provided or if message contains any of the common errors, + * update description to include original message plus the description + */ + protected setDescriptiveErrorMessage( + message: string, + description: string | undefined | null, + code?: string | null, + messageMapping?: { [key: string]: string }, + ) { + let newMessage = message; + let newDescription = description as string; + + if (messageMapping) { + for (const [mapKey, mapMessage] of Object.entries(messageMapping)) { + if ((message || '').toUpperCase().includes(mapKey.toUpperCase())) { + newMessage = mapMessage; + newDescription = this.updateDescription(message, description); + break; + } + } + if (newMessage !== message) { + return [newMessage, newDescription]; + } + } + + // if code is provided and it is in the list of common errors set the message and return early + if (code && COMMON_ERRORS[code.toUpperCase()]) { + newMessage = COMMON_ERRORS[code] as string; + newDescription = this.updateDescription(message, description); + return [newMessage, newDescription]; + } + + // check if message contains any of the common errors and set the message and description + for (const [errorCode, errorDescriptiveMessage] of Object.entries(COMMON_ERRORS)) { + if ((message || '').toUpperCase().includes(errorCode.toUpperCase())) { + newMessage = errorDescriptiveMessage as string; + newDescription = this.updateDescription(message, description); + break; + } + } + + return [newMessage, newDescription]; + } + + protected updateDescription(message: string, description: string | undefined | null) { + return `${message}${description ? ` - ${description}` : ''}`; + } +} diff --git a/packages/workflow/src/errors/reportable.error.ts b/packages/workflow/src/errors/application.error.ts similarity index 63% rename from packages/workflow/src/errors/reportable.error.ts rename to packages/workflow/src/errors/application.error.ts index b3d0fe5c20138..2b9673b924b98 100644 --- a/packages/workflow/src/errors/reportable.error.ts +++ b/packages/workflow/src/errors/application.error.ts @@ -6,16 +6,17 @@ export type ReportingOptions = { level?: Level; } & Pick; -export type ReportableErrorOptions = Partial & ReportingOptions; - -export class ReportableError extends Error { +export class ApplicationError extends Error { readonly level: Level; readonly tags?: Event['tags']; readonly extra?: Event['extra']; - constructor(message: string, { level, tags, extra, ...rest }: ReportableErrorOptions) { + constructor( + message: string, + { level, tags, extra, ...rest }: Partial & ReportingOptions = {}, + ) { super(message, rest); this.level = level ?? 'error'; this.tags = tags; diff --git a/packages/workflow/src/errors/cli-subworkflow-operation.error.ts b/packages/workflow/src/errors/cli-subworkflow-operation.error.ts new file mode 100644 index 0000000000000..59236957c6666 --- /dev/null +++ b/packages/workflow/src/errors/cli-subworkflow-operation.error.ts @@ -0,0 +1,3 @@ +import { SubworkflowOperationError } from './subworkflow-operation.error'; + +export class CliWorkflowOperationError extends SubworkflowOperationError {} diff --git a/packages/workflow/src/errors/expression-extension.error.ts b/packages/workflow/src/errors/expression-extension.error.ts new file mode 100644 index 0000000000000..f5f60b3c2e418 --- /dev/null +++ b/packages/workflow/src/errors/expression-extension.error.ts @@ -0,0 +1,3 @@ +import { ExpressionError } from './expression.error'; + +export class ExpressionExtensionError extends ExpressionError {} diff --git a/packages/workflow/src/ExpressionError.ts b/packages/workflow/src/errors/expression.error.ts similarity index 85% rename from packages/workflow/src/ExpressionError.ts rename to packages/workflow/src/errors/expression.error.ts index 7e06065424be4..1b647018a891e 100644 --- a/packages/workflow/src/ExpressionError.ts +++ b/packages/workflow/src/errors/expression.error.ts @@ -1,5 +1,5 @@ -import type { IDataObject } from './Interfaces'; -import { ExecutionBaseError } from './NodeErrors'; +import type { IDataObject } from '../Interfaces'; +import { ExecutionBaseError } from './abstract/execution-base.error'; /** * Class for instantiating an expression error @@ -47,5 +47,3 @@ export class ExpressionError extends ExecutionBaseError { } } } - -export class ExpressionExtensionError extends ExpressionError {} diff --git a/packages/workflow/src/errors/index.ts b/packages/workflow/src/errors/index.ts index 7cb82861d6c86..813a27124adae 100644 --- a/packages/workflow/src/errors/index.ts +++ b/packages/workflow/src/errors/index.ts @@ -1 +1,15 @@ -export { ReportableError } from './reportable.error'; +export { ApplicationError } from './application.error'; +export { ExpressionError } from './expression.error'; +export { NodeApiError } from './node-api.error'; +export { NodeOperationError } from './node-operation.error'; +export { NodeSslError } from './node-ssl.error'; +export { WebhookTakenError } from './webhook-taken.error'; +export { WorkflowActivationError } from './workflow-activation.error'; +export { WorkflowDeactivationError } from './workflow-deactivation.error'; +export { WorkflowOperationError } from './workflow-operation.error'; +export { SubworkflowOperationError } from './subworkflow-operation.error'; +export { CliWorkflowOperationError } from './cli-subworkflow-operation.error'; + +export { NodeError } from './abstract/node.error'; +export { ExecutionBaseError } from './abstract/execution-base.error'; +export { ExpressionExtensionError } from './expression-extension.error'; diff --git a/packages/workflow/src/errors/node-api.error.ts b/packages/workflow/src/errors/node-api.error.ts new file mode 100644 index 0000000000000..247cb0cc7e3b4 --- /dev/null +++ b/packages/workflow/src/errors/node-api.error.ts @@ -0,0 +1,267 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/naming-convention */ + +import { parseString } from 'xml2js'; +import type { INode, JsonObject, IDataObject, IStatusCodeMessages, Severity } from '..'; +import { NodeError } from './abstract/node.error'; +import { removeCircularRefs } from '../utils'; + +export interface NodeOperationErrorOptions { + message?: string; + description?: string; + runIndex?: number; + itemIndex?: number; + severity?: Severity; + messageMapping?: { [key: string]: string }; // allows to pass custom mapping for error messages scoped to a node +} + +interface NodeApiErrorOptions extends NodeOperationErrorOptions { + message?: string; + httpCode?: string; + parseXml?: boolean; +} + +/** + * Top-level properties where an error message can be found in an API response. + */ +const ERROR_MESSAGE_PROPERTIES = [ + 'cause', + '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']; + +/** + * Descriptive messages for common HTTP status codes + * this is used by NodeApiError class + */ +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', + + '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 - try again later or consider setting this node to retry automatically (in the node settings)', + '504': 'Gateway timed out - perhaps try again later?', +}; + +const UNKNOWN_ERROR_MESSAGE = 'UNKNOWN ERROR - check the detailed error for more information'; +const UNKNOWN_ERROR_MESSAGE_CRED = 'UNKNOWN ERROR'; + +/** + * 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, + runIndex, + itemIndex, + severity, + messageMapping, + }: NodeApiErrorOptions = {}, + ) { + super(node, error); + + // only for request library error + if (error.error) { + removeCircularRefs(error.error as JsonObject); + } + + // if not description provided, try to find it in the error object + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (!description && (error.description || (error?.reason as IDataObject)?.description)) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + this.description = (error.description || + (error?.reason as IDataObject)?.description) as string; + } + + // if not message provided, try to find it in the error object or set description as message + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (!message && (error.message || (error?.reason as IDataObject)?.message || description)) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + this.message = (error.message || + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + (error?.reason as IDataObject)?.message || + description) as string; + } + + // if it's an error generated by axios + // look for descriptions in the response object + if (error.reason) { + const reason: IDataObject = error.reason as unknown as IDataObject; + + if (reason.isAxiosError && reason.response) { + error = reason.response as JsonObject; + } + } + + // set http code of this error + if (httpCode) { + this.httpCode = httpCode; + } else { + this.httpCode = + this.findProperty(error, ERROR_STATUS_PROPERTIES, ERROR_NESTING_PROPERTIES) ?? null; + } + + if (severity) { + this.severity = severity; + } else if (this.httpCode?.charAt(0) !== '5') { + this.severity = 'warning'; + } + + // set description of this error + if (description) { + this.description = description; + } + + if (!this.description) { + if (parseXml) { + this.setDescriptionFromXml(error.error as string); + } else { + this.description = this.findProperty( + error, + ERROR_MESSAGE_PROPERTIES, + ERROR_NESTING_PROPERTIES, + ); + } + } + + // set message if provided or set default message based on http code + if (message) { + this.message = message; + } else { + this.setDefaultStatusCodeMessage(); + } + + // if message and description are the same, unset redundant description + if (this.message === this.description) { + this.description = undefined; + } + + // if message contain common error code set descriptive message and update description + [this.message, this.description] = this.setDescriptiveErrorMessage( + this.message, + this.description, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + this.httpCode || + (error?.code as string) || + ((error?.reason as JsonObject)?.code as string) || + undefined, + messageMapping, + ); + + if (runIndex !== undefined) this.context.runIndex = runIndex; + if (itemIndex !== undefined) this.context.itemIndex = itemIndex; + } + + private setDescriptionFromXml(xml: string) { + parseString(xml, { explicitArray: false }, (_, result) => { + if (!result) return; + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + const topLevelKey = Object.keys(result)[0]; + this.description = this.findProperty( + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access + result[topLevelKey], + ERROR_MESSAGE_PROPERTIES, + ['Error'].concat(ERROR_NESTING_PROPERTIES), + ); + }); + } + + /** + * Set the error's message based on the HTTP status code. + */ + private setDefaultStatusCodeMessage() { + // Set generic error message for 502 Bad Gateway + if (!this.httpCode && this.message && this.message.toLowerCase().includes('bad gateway')) { + this.httpCode = '502'; + } + + if (!this.httpCode) { + this.httpCode = null; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + this.message = this.message || this.description || UNKNOWN_ERROR_MESSAGE; + return; + } + + if (STATUS_CODE_MESSAGES[this.httpCode]) { + this.description = this.updateDescription(this.message, this.description); + this.message = STATUS_CODE_MESSAGES[this.httpCode]; + return; + } + + switch (this.httpCode.charAt(0)) { + case '4': + this.description = this.updateDescription(this.message, this.description); + this.message = STATUS_CODE_MESSAGES['4XX']; + break; + case '5': + this.description = this.updateDescription(this.message, this.description); + this.message = STATUS_CODE_MESSAGES['5XX']; + break; + default: + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + this.message = this.message || this.description || UNKNOWN_ERROR_MESSAGE; + } + if (this.node.type === 'n8n-nodes-base.noOp' && this.message === UNKNOWN_ERROR_MESSAGE) { + this.message = `${UNKNOWN_ERROR_MESSAGE_CRED} - ${this.httpCode}`; + } + } +} diff --git a/packages/workflow/src/errors/node-operation.error.ts b/packages/workflow/src/errors/node-operation.error.ts new file mode 100644 index 0000000000000..8f7ab00649841 --- /dev/null +++ b/packages/workflow/src/errors/node-operation.error.ts @@ -0,0 +1,34 @@ +import type { INode } from '..'; +import type { NodeOperationErrorOptions } from './node-api.error'; +import { NodeError } from './abstract/node.error'; + +/** + * Class for instantiating an operational error, e.g. an invalid credentials error. + */ +export class NodeOperationError extends NodeError { + lineNumber: number | undefined; + + constructor(node: INode, error: Error | string, options: NodeOperationErrorOptions = {}) { + if (typeof error === 'string') { + error = new Error(error); + } + super(node, error); + + if (options.message) this.message = options.message; + if (options.severity) this.severity = options.severity; + this.description = options.description; + this.context.runIndex = options.runIndex; + this.context.itemIndex = options.itemIndex; + + if (this.message === this.description) { + this.description = undefined; + } + + [this.message, this.description] = this.setDescriptiveErrorMessage( + this.message, + this.description, + undefined, + options.messageMapping, + ); + } +} diff --git a/packages/workflow/src/errors/node-ssl.error.ts b/packages/workflow/src/errors/node-ssl.error.ts new file mode 100644 index 0000000000000..2a16a20f1a5d1 --- /dev/null +++ b/packages/workflow/src/errors/node-ssl.error.ts @@ -0,0 +1,7 @@ +import { ExecutionBaseError } from './abstract/execution-base.error'; + +export class NodeSslError extends ExecutionBaseError { + constructor(cause: Error) { + super("SSL Issue: consider using the 'Ignore SSL issues' option", { cause }); + } +} diff --git a/packages/workflow/src/errors/subworkflow-operation.error.ts b/packages/workflow/src/errors/subworkflow-operation.error.ts new file mode 100644 index 0000000000000..3c4e7ca13ec7e --- /dev/null +++ b/packages/workflow/src/errors/subworkflow-operation.error.ts @@ -0,0 +1,18 @@ +import { WorkflowOperationError } from './workflow-operation.error'; + +export class SubworkflowOperationError extends WorkflowOperationError { + description = ''; + + cause: { message: string; stack: string }; + + constructor(message: string, description: string) { + super(message); + this.name = this.constructor.name; + this.description = description; + + this.cause = { + message, + stack: this.stack as string, + }; + } +} diff --git a/packages/workflow/src/errors/webhook-taken.error.ts b/packages/workflow/src/errors/webhook-taken.error.ts new file mode 100644 index 0000000000000..a30b8fa377c6d --- /dev/null +++ b/packages/workflow/src/errors/webhook-taken.error.ts @@ -0,0 +1,10 @@ +import { WorkflowActivationError } from './workflow-activation.error'; + +export class WebhookTakenError extends WorkflowActivationError { + constructor(nodeName: string, cause?: Error) { + super( + `The URL path that the "${nodeName}" node uses is already taken. Please change it to something else.`, + { severity: 'warning', cause }, + ); + } +} diff --git a/packages/workflow/src/WorkflowActivationError.ts b/packages/workflow/src/errors/workflow-activation.error.ts similarity index 63% rename from packages/workflow/src/WorkflowActivationError.ts rename to packages/workflow/src/errors/workflow-activation.error.ts index 3b664b21bbb37..3ce2a26636b86 100644 --- a/packages/workflow/src/WorkflowActivationError.ts +++ b/packages/workflow/src/errors/workflow-activation.error.ts @@ -1,5 +1,5 @@ -import type { INode } from './Interfaces'; -import { ExecutionBaseError, type Severity } from './NodeErrors'; +import type { INode, Severity } from '../Interfaces'; +import { ExecutionBaseError } from './abstract/execution-base.error'; interface WorkflowActivationErrorOptions { cause?: Error; @@ -34,14 +34,3 @@ export class WorkflowActivationError extends ExecutionBaseError { if (severity) this.severity = severity; } } - -export class WorkflowDeactivationError extends WorkflowActivationError {} - -export class WebhookPathAlreadyTakenError extends WorkflowActivationError { - constructor(nodeName: string, cause?: Error) { - super( - `The URL path that the "${nodeName}" node uses is already taken. Please change it to something else.`, - { severity: 'warning', cause }, - ); - } -} diff --git a/packages/workflow/src/errors/workflow-deactivation.error.ts b/packages/workflow/src/errors/workflow-deactivation.error.ts new file mode 100644 index 0000000000000..275544fd2b226 --- /dev/null +++ b/packages/workflow/src/errors/workflow-deactivation.error.ts @@ -0,0 +1,3 @@ +import { WorkflowActivationError } from './workflow-activation.error'; + +export class WorkflowDeactivationError extends WorkflowActivationError {} diff --git a/packages/workflow/src/errors/workflow-operation.error.ts b/packages/workflow/src/errors/workflow-operation.error.ts new file mode 100644 index 0000000000000..5bac19ac075b2 --- /dev/null +++ b/packages/workflow/src/errors/workflow-operation.error.ts @@ -0,0 +1,23 @@ +import type { INode } from '..'; +import { ExecutionBaseError } from './abstract/execution-base.error'; + +/** + * Class for instantiating an operational error, e.g. a timeout error. + */ +export class WorkflowOperationError extends ExecutionBaseError { + node: INode | undefined; + + timestamp: number; + + lineNumber: number | undefined; + + description: string | undefined; + + constructor(message: string, node?: INode) { + super(message, { cause: undefined }); + this.severity = 'warning'; + this.name = this.constructor.name; + this.node = node; + this.timestamp = Date.now(); + } +} diff --git a/packages/workflow/src/index.ts b/packages/workflow/src/index.ts index 9ac6bb4ddde3d..e7aecb0ece4b4 100644 --- a/packages/workflow/src/index.ts +++ b/packages/workflow/src/index.ts @@ -15,14 +15,10 @@ export * from './Interfaces'; export * from './MessageEventBus'; export * from './ExecutionStatus'; export * from './Expression'; -export * from './ExpressionError'; -export * from './NodeErrors'; export * from './NodeHelpers'; export * from './RoutingNode'; export * from './Workflow'; -export * from './WorkflowActivationError'; export * from './WorkflowDataProxy'; -export * from './WorkflowErrors'; export * from './WorkflowHooks'; export * from './VersionedNodeType'; export { LoggerProxy, NodeHelpers, ObservableObject, TelemetryHelpers }; diff --git a/packages/workflow/test/Expression.test.ts b/packages/workflow/test/Expression.test.ts index a5c191ce606a0..a224f9f8dd464 100644 --- a/packages/workflow/test/Expression.test.ts +++ b/packages/workflow/test/Expression.test.ts @@ -9,7 +9,7 @@ import type { ExpressionTestEvaluation, ExpressionTestTransform } from './Expres import { baseFixtures } from './ExpressionFixtures/base'; import type { INodeExecutionData } from '@/Interfaces'; import { extendSyntax } from '@/Extensions/ExpressionExtension'; -import { ExpressionError } from '@/ExpressionError'; +import { ExpressionError } from '@/errors/expression.error'; import { setDifferEnabled, setEvaluator } from '@/ExpressionEvaluatorProxy'; setDifferEnabled(true); diff --git a/packages/workflow/test/NodeErrors.test.ts b/packages/workflow/test/NodeErrors.test.ts index 04b9abc8b8ece..dcfa6a29690b1 100644 --- a/packages/workflow/test/NodeErrors.test.ts +++ b/packages/workflow/test/NodeErrors.test.ts @@ -1,5 +1,5 @@ import type { INode } from '@/Interfaces'; -import { NodeApiError, NodeOperationError } from '@/NodeErrors'; +import { NodeApiError, NodeOperationError } from '@/errors/abstract/execution-base.error'; const node: INode = { id: '1', diff --git a/packages/workflow/test/WorkflowDataProxy.test.ts b/packages/workflow/test/WorkflowDataProxy.test.ts index 4e316caf7995a..b681a80b06f30 100644 --- a/packages/workflow/test/WorkflowDataProxy.test.ts +++ b/packages/workflow/test/WorkflowDataProxy.test.ts @@ -2,7 +2,7 @@ import type { IConnections, IExecuteData, INode, IRunExecutionData } from '@/Int import { Workflow } from '@/Workflow'; import { WorkflowDataProxy } from '@/WorkflowDataProxy'; import * as Helpers from './Helpers'; -import { ExpressionError } from '@/ExpressionError'; +import { ExpressionError } from '@/errors/expression.error'; describe('WorkflowDataProxy', () => { describe('test data proxy', () => { From 9bd387db51af09fb1ae5cfec286efbfd376369e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 27 Nov 2023 10:39:55 +0100 Subject: [PATCH 2/6] Fix import in test --- packages/workflow/test/NodeErrors.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/workflow/test/NodeErrors.test.ts b/packages/workflow/test/NodeErrors.test.ts index dcfa6a29690b1..547a8d0533aac 100644 --- a/packages/workflow/test/NodeErrors.test.ts +++ b/packages/workflow/test/NodeErrors.test.ts @@ -1,5 +1,6 @@ import type { INode } from '@/Interfaces'; -import { NodeApiError, NodeOperationError } from '@/errors/abstract/execution-base.error'; +import { NodeOperationError } from '@/errors'; +import { NodeApiError } from '@/errors/node-api.error'; const node: INode = { id: '1', From 4901026416ec7384822400fa961d23682b3e71d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 27 Nov 2023 10:42:52 +0100 Subject: [PATCH 3/6] Fix more imports in tests --- packages/core/test/WorkflowExecutionMetadata.test.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/test/WorkflowExecutionMetadata.test.ts b/packages/core/test/WorkflowExecutionMetadata.test.ts index ce6290d421903..f806df6ef6f84 100644 --- a/packages/core/test/WorkflowExecutionMetadata.test.ts +++ b/packages/core/test/WorkflowExecutionMetadata.test.ts @@ -1,11 +1,11 @@ import { - getAllWorkflowExecutionMetadata, - getWorkflowExecutionMetadata, - KV_LIMIT, - setAllWorkflowExecutionMetadata, setWorkflowExecutionMetadata, - InvalidExecutionMetadata, -} from '@/errors/invalid-execution-metadata.error'; + setAllWorkflowExecutionMetadata, + KV_LIMIT, + getWorkflowExecutionMetadata, + getAllWorkflowExecutionMetadata, +} from '@/ExecutionMetadata'; +import { InvalidExecutionMetadata } from '@/errors/invalid-execution-metadata.error'; import type { IRunExecutionData } from 'n8n-workflow'; describe('Execution Metadata functions', () => { From 6d968af0cd65fcbc9e3358b7344e52773ee588f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 27 Nov 2023 13:37:57 +0100 Subject: [PATCH 4/6] Rename `WebhookTakenError` to `WebhookPathTakenError` --- packages/cli/src/ActiveWebhooks.ts | 4 ++-- packages/cli/src/ActiveWorkflowRunner.ts | 4 ++-- packages/workflow/src/errors/index.ts | 2 +- packages/workflow/src/errors/webhook-taken.error.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/ActiveWebhooks.ts b/packages/cli/src/ActiveWebhooks.ts index 54705a0c17222..0b49a07b9b5d4 100644 --- a/packages/cli/src/ActiveWebhooks.ts +++ b/packages/cli/src/ActiveWebhooks.ts @@ -6,7 +6,7 @@ import type { WorkflowActivateMode, WorkflowExecuteMode, } from 'n8n-workflow'; -import { WebhookTakenError } from 'n8n-workflow'; +import { WebhookPathTakenError } from 'n8n-workflow'; import * as NodeExecuteFunctions from 'n8n-core'; @Service() @@ -46,7 +46,7 @@ export class ActiveWebhooks { // check that there is not a webhook already registered with that path/method if (this.webhookUrls[webhookKey] && !webhookData.webhookId) { - throw new WebhookTakenError(webhookData.node); + throw new WebhookPathTakenError(webhookData.node); } if (this.workflowWebhooks[webhookData.workflowId] === undefined) { diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 2f55afda0eb78..efa7e47521ad1 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -29,7 +29,7 @@ import { Workflow, WorkflowActivationError, ErrorReporterProxy as ErrorReporter, - WebhookTakenError, + WebhookPathTakenError, } from 'n8n-workflow'; import type express from 'express'; @@ -403,7 +403,7 @@ export class ActiveWorkflowRunner implements IWebhookManager { // TODO check if there is standard error code for duplicate key violation that works // with all databases if (error instanceof Error && error.name === 'QueryFailedError') { - error = new WebhookTakenError(webhook.node, error); + error = new WebhookPathTakenError(webhook.node, error); } else if (error.detail) { // it's a error running the webhook methods (checkExists, create) error.message = error.detail; diff --git a/packages/workflow/src/errors/index.ts b/packages/workflow/src/errors/index.ts index 813a27124adae..8cf0b8de0cbf3 100644 --- a/packages/workflow/src/errors/index.ts +++ b/packages/workflow/src/errors/index.ts @@ -3,7 +3,7 @@ export { ExpressionError } from './expression.error'; export { NodeApiError } from './node-api.error'; export { NodeOperationError } from './node-operation.error'; export { NodeSslError } from './node-ssl.error'; -export { WebhookTakenError } from './webhook-taken.error'; +export { WebhookPathTakenError } from './webhook-taken.error'; export { WorkflowActivationError } from './workflow-activation.error'; export { WorkflowDeactivationError } from './workflow-deactivation.error'; export { WorkflowOperationError } from './workflow-operation.error'; diff --git a/packages/workflow/src/errors/webhook-taken.error.ts b/packages/workflow/src/errors/webhook-taken.error.ts index a30b8fa377c6d..c19ac4cb5040b 100644 --- a/packages/workflow/src/errors/webhook-taken.error.ts +++ b/packages/workflow/src/errors/webhook-taken.error.ts @@ -1,6 +1,6 @@ import { WorkflowActivationError } from './workflow-activation.error'; -export class WebhookTakenError extends WorkflowActivationError { +export class WebhookPathTakenError extends WorkflowActivationError { constructor(nodeName: string, cause?: Error) { super( `The URL path that the "${nodeName}" node uses is already taken. Please change it to something else.`, From af5cf4803e4dcbc839ecc054558fb784cb2fb08c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 27 Nov 2023 13:39:05 +0100 Subject: [PATCH 5/6] Rename `InvalidExecutionMetadata` to `InvalidExecutionMetadataError` --- packages/core/src/ExecutionMetadata.ts | 8 ++++---- packages/core/src/errors/index.ts | 2 +- .../src/errors/invalid-execution-metadata.error.ts | 2 +- packages/core/test/WorkflowExecutionMetadata.test.ts | 10 +++++----- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/core/src/ExecutionMetadata.ts b/packages/core/src/ExecutionMetadata.ts index 50c1ccbc11024..cc2743a56f923 100644 --- a/packages/core/src/ExecutionMetadata.ts +++ b/packages/core/src/ExecutionMetadata.ts @@ -1,6 +1,6 @@ import type { IRunExecutionData } from 'n8n-workflow'; import { LoggerProxy as Logger } from 'n8n-workflow'; -import { InvalidExecutionMetadata } from './errors/invalid-execution-metadata.error'; +import { InvalidExecutionMetadataError } from './errors/invalid-execution-metadata.error'; export const KV_LIMIT = 10; @@ -20,17 +20,17 @@ export function setWorkflowExecutionMetadata( return; } if (typeof key !== 'string') { - throw new InvalidExecutionMetadata('key', key); + throw new InvalidExecutionMetadataError('key', key); } if (key.replace(/[A-Za-z0-9_]/g, '').length !== 0) { - throw new InvalidExecutionMetadata( + throw new InvalidExecutionMetadataError( 'key', key, `Custom date key can only contain characters "A-Za-z0-9_" (key "${key}")`, ); } if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'bigint') { - throw new InvalidExecutionMetadata('value', key); + throw new InvalidExecutionMetadataError('value', key); } const val = String(value); if (key.length > 50) { diff --git a/packages/core/src/errors/index.ts b/packages/core/src/errors/index.ts index 1370daf3b3f01..c280ecb30dcba 100644 --- a/packages/core/src/errors/index.ts +++ b/packages/core/src/errors/index.ts @@ -2,4 +2,4 @@ export { FileNotFoundError } from './file-not-found.error'; export { DisallowedFilepathError } from './disallowed-filepath.error'; export { InvalidModeError } from './invalid-mode.error'; export { InvalidManagerError } from './invalid-manager.error'; -export { InvalidExecutionMetadata } from './invalid-execution-metadata.error'; +export { InvalidExecutionMetadataError } from './invalid-execution-metadata.error'; diff --git a/packages/core/src/errors/invalid-execution-metadata.error.ts b/packages/core/src/errors/invalid-execution-metadata.error.ts index 3dde839525a48..972703e0d195e 100644 --- a/packages/core/src/errors/invalid-execution-metadata.error.ts +++ b/packages/core/src/errors/invalid-execution-metadata.error.ts @@ -1,6 +1,6 @@ import { ApplicationError } from 'n8n-workflow'; -export class InvalidExecutionMetadata extends ApplicationError { +export class InvalidExecutionMetadataError extends ApplicationError { constructor( public type: 'key' | 'value', key: unknown, diff --git a/packages/core/test/WorkflowExecutionMetadata.test.ts b/packages/core/test/WorkflowExecutionMetadata.test.ts index f806df6ef6f84..cdb50c9737230 100644 --- a/packages/core/test/WorkflowExecutionMetadata.test.ts +++ b/packages/core/test/WorkflowExecutionMetadata.test.ts @@ -5,7 +5,7 @@ import { getWorkflowExecutionMetadata, getAllWorkflowExecutionMetadata, } from '@/ExecutionMetadata'; -import { InvalidExecutionMetadata } from '@/errors/invalid-execution-metadata.error'; +import { InvalidExecutionMetadataError } from '@/errors/invalid-execution-metadata.error'; import type { IRunExecutionData } from 'n8n-workflow'; describe('Execution Metadata functions', () => { @@ -52,7 +52,7 @@ describe('Execution Metadata functions', () => { } as IRunExecutionData; expect(() => setWorkflowExecutionMetadata(executionData, 'test1', 1234)).not.toThrow( - InvalidExecutionMetadata, + InvalidExecutionMetadataError, ); expect(metadata).toEqual({ @@ -60,7 +60,7 @@ describe('Execution Metadata functions', () => { }); expect(() => setWorkflowExecutionMetadata(executionData, 'test2', {})).toThrow( - InvalidExecutionMetadata, + InvalidExecutionMetadataError, ); expect(metadata).not.toEqual({ @@ -84,7 +84,7 @@ describe('Execution Metadata functions', () => { test3: 'value3', test4: 'value4', }), - ).toThrow(InvalidExecutionMetadata); + ).toThrow(InvalidExecutionMetadataError); expect(metadata).toEqual({ test3: 'value3', @@ -101,7 +101,7 @@ describe('Execution Metadata functions', () => { } as IRunExecutionData; expect(() => setWorkflowExecutionMetadata(executionData, 'te$t1$', 1234)).toThrow( - InvalidExecutionMetadata, + InvalidExecutionMetadataError, ); expect(metadata).not.toEqual({ From ff574e1e2c4671a87e0705803fadce5ce3c9e041 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Mon, 27 Nov 2023 13:40:07 +0100 Subject: [PATCH 6/6] Add tech debt comment to `ExecutionBaseError.cause` --- packages/workflow/src/errors/abstract/execution-base.error.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/workflow/src/errors/abstract/execution-base.error.ts b/packages/workflow/src/errors/abstract/execution-base.error.ts index c1de0ab54fe4e..deeccad9e53f3 100644 --- a/packages/workflow/src/errors/abstract/execution-base.error.ts +++ b/packages/workflow/src/errors/abstract/execution-base.error.ts @@ -8,6 +8,9 @@ interface ExecutionBaseErrorOptions { export abstract class ExecutionBaseError extends ApplicationError { description: string | null | undefined; + /** + * @tech_debt Ensure `cause` can only be `Error` or `undefined` + */ cause: Error | JsonObject | undefined; timestamp: number;