mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
refactor(core): Reorganize error hierarchy in core
and workflow
packages (no-changelog) (#7820)
Ensure all errors in `core` and `workflow` inherit from `ApplicationError` so that we start normalizing all the errors we report to Sentry Follow-up to: https://github.com/n8n-io/n8n/pull/7757#discussion_r1404338844 ### `core` package `ApplicationError` - `FileSystemError` (abstract) - `FileNotFoundError` - `DisallowedFilepathError` - `BinaryDataError` (abstract) - `InvalidModeError` - `InvalidManagerError` - `InvalidExecutionMetadataError` ### `workflow` package `ApplicationError` - `ExecutionBaseError` (abstract) - `WorkflowActivationError` - `WorkflowDeactivationError` - `WebhookTakenError` - `WorkflowOperationError` - `SubworkflowOperationError` - `CliWorkflowOperationError` - `ExpressionError` - `ExpressionExtensionError` - `NodeError` (abstract) - `NodeOperationError` - `NodeApiError` - `NodeSSLError` Up next: - Reorganize errors in `cli` - Flatten the hierarchy in `workflow` (do we really need `ExecutionBaseError`?) - Remove `ExecutionError` type - Stop throwing plain `Error`s - Replace `severity` with `level` - Add node and credential types as `tags` - Add workflow IDs and execution IDs as `extras`
This commit is contained in:
parent
aae45b043b
commit
dff8456382
|
@ -6,7 +6,7 @@ import type {
|
||||||
WorkflowActivateMode,
|
WorkflowActivateMode,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { WebhookPathAlreadyTakenError } from 'n8n-workflow';
|
import { WebhookPathTakenError } from 'n8n-workflow';
|
||||||
import * as NodeExecuteFunctions from 'n8n-core';
|
import * as NodeExecuteFunctions from 'n8n-core';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
|
@ -46,7 +46,7 @@ export class ActiveWebhooks {
|
||||||
|
|
||||||
// check that there is not a webhook already registered with that path/method
|
// check that there is not a webhook already registered with that path/method
|
||||||
if (this.webhookUrls[webhookKey] && !webhookData.webhookId) {
|
if (this.webhookUrls[webhookKey] && !webhookData.webhookId) {
|
||||||
throw new WebhookPathAlreadyTakenError(webhookData.node);
|
throw new WebhookPathTakenError(webhookData.node);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.workflowWebhooks[webhookData.workflowId] === undefined) {
|
if (this.workflowWebhooks[webhookData.workflowId] === undefined) {
|
||||||
|
|
|
@ -29,7 +29,7 @@ import {
|
||||||
Workflow,
|
Workflow,
|
||||||
WorkflowActivationError,
|
WorkflowActivationError,
|
||||||
ErrorReporterProxy as ErrorReporter,
|
ErrorReporterProxy as ErrorReporter,
|
||||||
WebhookPathAlreadyTakenError,
|
WebhookPathTakenError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import type express from 'express';
|
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
|
// TODO check if there is standard error code for duplicate key violation that works
|
||||||
// with all databases
|
// with all databases
|
||||||
if (error instanceof Error && error.name === 'QueryFailedError') {
|
if (error instanceof Error && error.name === 'QueryFailedError') {
|
||||||
error = new WebhookPathAlreadyTakenError(webhook.node, error);
|
error = new WebhookPathTakenError(webhook.node, error);
|
||||||
} else if (error.detail) {
|
} else if (error.detail) {
|
||||||
// it's a error running the webhook methods (checkExists, create)
|
// it's a error running the webhook methods (checkExists, create)
|
||||||
error.message = error.detail;
|
error.message = error.detail;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import { ErrorReporterProxy, ExecutionBaseError, ReportableError } from 'n8n-workflow';
|
import { ErrorReporterProxy, ApplicationError, ExecutionBaseError } from 'n8n-workflow';
|
||||||
|
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ export const initErrorHandling = async () => {
|
||||||
if (originalException instanceof ExecutionBaseError && originalException.severity === 'warning')
|
if (originalException instanceof ExecutionBaseError && originalException.severity === 'warning')
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (originalException instanceof ReportableError) {
|
if (originalException instanceof ApplicationError) {
|
||||||
const { level, extra } = originalException;
|
const { level, extra } = originalException;
|
||||||
if (level === 'warning') return null;
|
if (level === 'warning') return null;
|
||||||
event.level = level;
|
event.level = level;
|
||||||
|
|
|
@ -2,12 +2,13 @@ import { readFile, stat } from 'node:fs/promises';
|
||||||
import prettyBytes from 'pretty-bytes';
|
import prettyBytes from 'pretty-bytes';
|
||||||
import Container, { Service } from 'typedi';
|
import Container, { Service } from 'typedi';
|
||||||
import { BINARY_ENCODING } from 'n8n-workflow';
|
import { BINARY_ENCODING } from 'n8n-workflow';
|
||||||
import { UnknownManagerError, InvalidModeError } from './errors';
|
import { InvalidModeError } from '../errors/invalid-mode.error';
|
||||||
import { areConfigModes, toBuffer } from './utils';
|
import { areConfigModes, toBuffer } from './utils';
|
||||||
|
|
||||||
import type { Readable } from 'stream';
|
import type { Readable } from 'stream';
|
||||||
import type { BinaryData } from './types';
|
import type { BinaryData } from './types';
|
||||||
import type { INodeExecutionData, IBinaryData } from 'n8n-workflow';
|
import type { INodeExecutionData, IBinaryData } from 'n8n-workflow';
|
||||||
|
import { InvalidManagerError } from '../errors/invalid-manager.error';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
export class BinaryDataService {
|
export class BinaryDataService {
|
||||||
|
@ -241,6 +242,6 @@ export class BinaryDataService {
|
||||||
|
|
||||||
if (manager) return manager;
|
if (manager) return manager;
|
||||||
|
|
||||||
throw new UnknownManagerError(mode);
|
throw new InvalidManagerError(mode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,10 +4,11 @@ import path from 'node:path';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import { jsonParse } from 'n8n-workflow';
|
import { jsonParse } from 'n8n-workflow';
|
||||||
import { assertDir, doesNotExist } from './utils';
|
import { assertDir, doesNotExist } from './utils';
|
||||||
import { BinaryFileNotFoundError, InvalidPathError } from '../errors';
|
import { DisallowedFilepathError } from '../errors/disallowed-filepath.error';
|
||||||
|
|
||||||
import type { Readable } from 'stream';
|
import type { Readable } from 'stream';
|
||||||
import type { BinaryData } from './types';
|
import type { BinaryData } from './types';
|
||||||
|
import { FileNotFoundError } from '../errors/file-not-found.error';
|
||||||
|
|
||||||
const EXECUTION_ID_EXTRACTOR =
|
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})$/;
|
/^(\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);
|
const filePath = this.resolvePath(fileId);
|
||||||
|
|
||||||
if (await doesNotExist(filePath)) {
|
if (await doesNotExist(filePath)) {
|
||||||
throw new BinaryFileNotFoundError(filePath);
|
throw new FileNotFoundError(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return createReadStream(filePath, { highWaterMark: chunkSize });
|
return createReadStream(filePath, { highWaterMark: chunkSize });
|
||||||
|
@ -57,7 +58,7 @@ export class FileSystemManager implements BinaryData.Manager {
|
||||||
const filePath = this.resolvePath(fileId);
|
const filePath = this.resolvePath(fileId);
|
||||||
|
|
||||||
if (await doesNotExist(filePath)) {
|
if (await doesNotExist(filePath)) {
|
||||||
throw new BinaryFileNotFoundError(filePath);
|
throw new FileNotFoundError(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return fs.readFile(filePath);
|
return fs.readFile(filePath);
|
||||||
|
@ -171,7 +172,7 @@ export class FileSystemManager implements BinaryData.Manager {
|
||||||
const returnPath = path.join(this.storagePath, ...args);
|
const returnPath = path.join(this.storagePath, ...args);
|
||||||
|
|
||||||
if (path.relative(this.storagePath, returnPath).startsWith('..')) {
|
if (path.relative(this.storagePath, returnPath).startsWith('..')) {
|
||||||
throw new InvalidPathError(returnPath);
|
throw new DisallowedFilepathError(returnPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return returnPath;
|
return returnPath;
|
||||||
|
@ -190,7 +191,7 @@ export class FileSystemManager implements BinaryData.Manager {
|
||||||
const stats = await fs.stat(filePath);
|
const stats = await fs.stat(filePath);
|
||||||
return stats.size;
|
return stats.size;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new BinaryFileNotFoundError(filePath);
|
throw new FileNotFoundError(filePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}`);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,20 +1,9 @@
|
||||||
import type { IRunExecutionData } from 'n8n-workflow';
|
import type { IRunExecutionData } from 'n8n-workflow';
|
||||||
import { LoggerProxy as Logger } from 'n8n-workflow';
|
import { LoggerProxy as Logger } from 'n8n-workflow';
|
||||||
|
import { InvalidExecutionMetadataError } from './errors/invalid-execution-metadata.error';
|
||||||
|
|
||||||
export const KV_LIMIT = 10;
|
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(
|
export function setWorkflowExecutionMetadata(
|
||||||
executionData: IRunExecutionData,
|
executionData: IRunExecutionData,
|
||||||
key: string,
|
key: string,
|
||||||
|
@ -31,17 +20,17 @@ export function setWorkflowExecutionMetadata(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof key !== 'string') {
|
if (typeof key !== 'string') {
|
||||||
throw new ExecutionMetadataValidationError('key', key);
|
throw new InvalidExecutionMetadataError('key', key);
|
||||||
}
|
}
|
||||||
if (key.replace(/[A-Za-z0-9_]/g, '').length !== 0) {
|
if (key.replace(/[A-Za-z0-9_]/g, '').length !== 0) {
|
||||||
throw new ExecutionMetadataValidationError(
|
throw new InvalidExecutionMetadataError(
|
||||||
'key',
|
'key',
|
||||||
key,
|
key,
|
||||||
`Custom date key can only contain characters "A-Za-z0-9_" (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') {
|
if (typeof value !== 'string' && typeof value !== 'number' && typeof value !== 'bigint') {
|
||||||
throw new ExecutionMetadataValidationError('value', key);
|
throw new InvalidExecutionMetadataError('value', key);
|
||||||
}
|
}
|
||||||
const val = String(value);
|
const val = String(value);
|
||||||
if (key.length > 50) {
|
if (key.length > 50) {
|
|
@ -38,7 +38,6 @@ import type {
|
||||||
BinaryHelperFunctions,
|
BinaryHelperFunctions,
|
||||||
ConnectionTypes,
|
ConnectionTypes,
|
||||||
ContextType,
|
ContextType,
|
||||||
ExecutionError,
|
|
||||||
FieldType,
|
FieldType,
|
||||||
FileSystemHelperFunctions,
|
FileSystemHelperFunctions,
|
||||||
FunctionsBase,
|
FunctionsBase,
|
||||||
|
@ -101,7 +100,7 @@ import {
|
||||||
NodeApiError,
|
NodeApiError,
|
||||||
NodeHelpers,
|
NodeHelpers,
|
||||||
NodeOperationError,
|
NodeOperationError,
|
||||||
NodeSSLError,
|
NodeSslError,
|
||||||
OAuth2GrantType,
|
OAuth2GrantType,
|
||||||
WorkflowDataProxy,
|
WorkflowDataProxy,
|
||||||
createDeferredPromise,
|
createDeferredPromise,
|
||||||
|
@ -143,7 +142,7 @@ import {
|
||||||
getWorkflowExecutionMetadata,
|
getWorkflowExecutionMetadata,
|
||||||
setAllWorkflowExecutionMetadata,
|
setAllWorkflowExecutionMetadata,
|
||||||
setWorkflowExecutionMetadata,
|
setWorkflowExecutionMetadata,
|
||||||
} from './WorkflowExecutionMetadata';
|
} from './ExecutionMetadata';
|
||||||
import { getSecretsProxy } from './Secrets';
|
import { getSecretsProxy } from './Secrets';
|
||||||
import Container from 'typedi';
|
import Container from 'typedi';
|
||||||
import type { BinaryData } from './BinaryData/types';
|
import type { BinaryData } from './BinaryData/types';
|
||||||
|
@ -808,7 +807,7 @@ export async function proxyRequestToAxios(
|
||||||
response: pick(response, ['headers', 'status', 'statusText']),
|
response: pick(response, ['headers', 'status', 'statusText']),
|
||||||
});
|
});
|
||||||
} else if ('rejectUnauthorized' in configObject && error.code?.includes('CERT')) {
|
} 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(
|
addInputData(
|
||||||
connectionType: ConnectionTypes,
|
connectionType: ConnectionTypes,
|
||||||
data: INodeExecutionData[][] | ExecutionError,
|
data: INodeExecutionData[][] | ExecutionBaseError,
|
||||||
): { index: number } {
|
): { index: number } {
|
||||||
const nodeName = this.getNode().name;
|
const nodeName = this.getNode().name;
|
||||||
let currentNodeRunIndex = 0;
|
let currentNodeRunIndex = 0;
|
||||||
|
@ -3473,7 +3472,7 @@ export function getExecuteFunctions(
|
||||||
addOutputData(
|
addOutputData(
|
||||||
connectionType: ConnectionTypes,
|
connectionType: ConnectionTypes,
|
||||||
currentNodeRunIndex: number,
|
currentNodeRunIndex: number,
|
||||||
data: INodeExecutionData[][] | ExecutionError,
|
data: INodeExecutionData[][] | ExecutionBaseError,
|
||||||
): void {
|
): void {
|
||||||
addExecutionDataFunctions(
|
addExecutionDataFunctions(
|
||||||
'output',
|
'output',
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { setMaxListeners } from 'events';
|
||||||
import PCancelable from 'p-cancelable';
|
import PCancelable from 'p-cancelable';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ExecutionError,
|
ExecutionBaseError,
|
||||||
ExecutionStatus,
|
ExecutionStatus,
|
||||||
GenericValue,
|
GenericValue,
|
||||||
IConnection,
|
IConnection,
|
||||||
|
@ -797,7 +797,7 @@ export class WorkflowExecute {
|
||||||
|
|
||||||
// Variables which hold temporary data for each node-execution
|
// Variables which hold temporary data for each node-execution
|
||||||
let executionData: IExecuteData;
|
let executionData: IExecuteData;
|
||||||
let executionError: ExecutionError | undefined;
|
let executionError: ExecutionBaseError | undefined;
|
||||||
let executionNode: INode;
|
let executionNode: INode;
|
||||||
let nodeSuccessData: INodeExecutionData[][] | null | undefined;
|
let nodeSuccessData: INodeExecutionData[][] | null | undefined;
|
||||||
let runIndex: number;
|
let runIndex: number;
|
||||||
|
@ -838,11 +838,13 @@ export class WorkflowExecute {
|
||||||
await this.executeHook('workflowExecuteBefore', [workflow]);
|
await this.executeHook('workflowExecuteBefore', [workflow]);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const e = error as unknown as ExecutionBaseError;
|
||||||
|
|
||||||
// Set the error that it can be saved correctly
|
// Set the error that it can be saved correctly
|
||||||
executionError = {
|
executionError = {
|
||||||
...(error as NodeOperationError | NodeApiError),
|
...e,
|
||||||
message: (error as NodeOperationError | NodeApiError).message,
|
message: e.message,
|
||||||
stack: (error as NodeOperationError | NodeApiError).stack,
|
stack: e.stack,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set the incoming data of the node that it can be saved correctly
|
// Set the incoming data of the node that it can be saved correctly
|
||||||
|
@ -1253,10 +1255,12 @@ export class WorkflowExecute {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.runExecutionData.resultData.lastNodeExecuted = executionData.node.name;
|
this.runExecutionData.resultData.lastNodeExecuted = executionData.node.name;
|
||||||
|
|
||||||
|
const e = error as unknown as ExecutionBaseError;
|
||||||
|
|
||||||
executionError = {
|
executionError = {
|
||||||
...(error as NodeOperationError | NodeApiError),
|
...e,
|
||||||
message: (error as NodeOperationError | NodeApiError).message,
|
message: e.message,
|
||||||
stack: (error as NodeOperationError | NodeApiError).stack,
|
stack: e.stack,
|
||||||
};
|
};
|
||||||
|
|
||||||
Logger.debug(`Running node "${executionNode.name}" finished with error`, {
|
Logger.debug(`Running node "${executionNode.name}" finished with error`, {
|
||||||
|
@ -1325,11 +1329,17 @@ export class WorkflowExecute {
|
||||||
lineResult.json.$error !== undefined &&
|
lineResult.json.$error !== undefined &&
|
||||||
lineResult.json.$json !== 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.error = lineResult.json.$error as NodeApiError | NodeOperationError;
|
||||||
lineResult.json = {
|
lineResult.json = {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
error: (lineResult.json.$error as NodeApiError | NodeOperationError).message,
|
error: (lineResult.json.$error as NodeApiError | NodeOperationError).message,
|
||||||
};
|
};
|
||||||
} else if (lineResult.error !== undefined) {
|
} else if (lineResult.error !== undefined) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
lineResult.json = { error: lineResult.error.message };
|
lineResult.json = { error: lineResult.error.message };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1697,7 +1707,7 @@ export class WorkflowExecute {
|
||||||
async processSuccessExecution(
|
async processSuccessExecution(
|
||||||
startedAt: Date,
|
startedAt: Date,
|
||||||
workflow: Workflow,
|
workflow: Workflow,
|
||||||
executionError?: ExecutionError,
|
executionError?: ExecutionBaseError,
|
||||||
closeFunction?: Promise<void>,
|
closeFunction?: Promise<void>,
|
||||||
): Promise<IRun> {
|
): Promise<IRun> {
|
||||||
const fullRunData = this.getFullRunData(startedAt);
|
const fullRunData = this.getFullRunData(startedAt);
|
||||||
|
@ -1711,7 +1721,7 @@ export class WorkflowExecute {
|
||||||
...executionError,
|
...executionError,
|
||||||
message: executionError.message,
|
message: executionError.message,
|
||||||
stack: executionError.stack,
|
stack: executionError.stack,
|
||||||
} as ExecutionError;
|
} as ExecutionBaseError;
|
||||||
if (executionError.message?.includes('canceled')) {
|
if (executionError.message?.includes('canceled')) {
|
||||||
fullRunData.status = 'canceled';
|
fullRunData.status = 'canceled';
|
||||||
}
|
}
|
||||||
|
|
3
packages/core/src/errors/abstract/binary-data.error.ts
Normal file
3
packages/core/src/errors/abstract/binary-data.error.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import { ApplicationError } from 'n8n-workflow';
|
||||||
|
|
||||||
|
export abstract class BinaryDataError extends ApplicationError {}
|
7
packages/core/src/errors/abstract/filesystem.error.ts
Normal file
7
packages/core/src/errors/abstract/filesystem.error.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { ApplicationError } from 'n8n-workflow';
|
||||||
|
|
||||||
|
export abstract class FileSystemError extends ApplicationError {
|
||||||
|
constructor(message: string, filePath: string) {
|
||||||
|
super(message, { extra: { filePath } });
|
||||||
|
}
|
||||||
|
}
|
7
packages/core/src/errors/disallowed-filepath.error.ts
Normal file
7
packages/core/src/errors/disallowed-filepath.error.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { FileSystemError } from './abstract/filesystem.error';
|
||||||
|
|
||||||
|
export class DisallowedFilepathError extends FileSystemError {
|
||||||
|
constructor(filePath: string) {
|
||||||
|
super('Disallowed path detected', filePath);
|
||||||
|
}
|
||||||
|
}
|
7
packages/core/src/errors/file-not-found.error.ts
Normal file
7
packages/core/src/errors/file-not-found.error.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { FileSystemError } from './abstract/filesystem.error';
|
||||||
|
|
||||||
|
export class FileNotFoundError extends FileSystemError {
|
||||||
|
constructor(filePath: string) {
|
||||||
|
super('File not found', filePath);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 { InvalidExecutionMetadataError } from './invalid-execution-metadata.error';
|
||||||
|
|
13
packages/core/src/errors/invalid-execution-metadata.error.ts
Normal file
13
packages/core/src/errors/invalid-execution-metadata.error.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { ApplicationError } from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class InvalidExecutionMetadataError 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);
|
||||||
|
}
|
||||||
|
}
|
7
packages/core/src/errors/invalid-manager.error.ts
Normal file
7
packages/core/src/errors/invalid-manager.error.ts
Normal file
|
@ -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}`);
|
||||||
|
}
|
||||||
|
}
|
8
packages/core/src/errors/invalid-mode.error.ts
Normal file
8
packages/core/src/errors/invalid-mode.error.ts
Normal file
|
@ -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(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,3 +18,4 @@ export * from './errors';
|
||||||
export { ObjectStoreService } from './ObjectStore/ObjectStore.service.ee';
|
export { ObjectStoreService } from './ObjectStore/ObjectStore.service.ee';
|
||||||
export { BinaryData } from './BinaryData/types';
|
export { BinaryData } from './BinaryData/types';
|
||||||
export { isStoredMode as isValidNonDefaultMode } from './BinaryData/utils';
|
export { isStoredMode as isValidNonDefaultMode } from './BinaryData/utils';
|
||||||
|
export * from './ExecutionMetadata';
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import {
|
import {
|
||||||
getAllWorkflowExecutionMetadata,
|
|
||||||
getWorkflowExecutionMetadata,
|
|
||||||
KV_LIMIT,
|
|
||||||
setAllWorkflowExecutionMetadata,
|
|
||||||
setWorkflowExecutionMetadata,
|
setWorkflowExecutionMetadata,
|
||||||
ExecutionMetadataValidationError,
|
setAllWorkflowExecutionMetadata,
|
||||||
} from '@/WorkflowExecutionMetadata';
|
KV_LIMIT,
|
||||||
|
getWorkflowExecutionMetadata,
|
||||||
|
getAllWorkflowExecutionMetadata,
|
||||||
|
} from '@/ExecutionMetadata';
|
||||||
|
import { InvalidExecutionMetadataError } from '@/errors/invalid-execution-metadata.error';
|
||||||
import type { IRunExecutionData } from 'n8n-workflow';
|
import type { IRunExecutionData } from 'n8n-workflow';
|
||||||
|
|
||||||
describe('Execution Metadata functions', () => {
|
describe('Execution Metadata functions', () => {
|
||||||
|
@ -52,7 +52,7 @@ describe('Execution Metadata functions', () => {
|
||||||
} as IRunExecutionData;
|
} as IRunExecutionData;
|
||||||
|
|
||||||
expect(() => setWorkflowExecutionMetadata(executionData, 'test1', 1234)).not.toThrow(
|
expect(() => setWorkflowExecutionMetadata(executionData, 'test1', 1234)).not.toThrow(
|
||||||
ExecutionMetadataValidationError,
|
InvalidExecutionMetadataError,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(metadata).toEqual({
|
expect(metadata).toEqual({
|
||||||
|
@ -60,7 +60,7 @@ describe('Execution Metadata functions', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(() => setWorkflowExecutionMetadata(executionData, 'test2', {})).toThrow(
|
expect(() => setWorkflowExecutionMetadata(executionData, 'test2', {})).toThrow(
|
||||||
ExecutionMetadataValidationError,
|
InvalidExecutionMetadataError,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(metadata).not.toEqual({
|
expect(metadata).not.toEqual({
|
||||||
|
@ -84,7 +84,7 @@ describe('Execution Metadata functions', () => {
|
||||||
test3: 'value3',
|
test3: 'value3',
|
||||||
test4: 'value4',
|
test4: 'value4',
|
||||||
}),
|
}),
|
||||||
).toThrow(ExecutionMetadataValidationError);
|
).toThrow(InvalidExecutionMetadataError);
|
||||||
|
|
||||||
expect(metadata).toEqual({
|
expect(metadata).toEqual({
|
||||||
test3: 'value3',
|
test3: 'value3',
|
||||||
|
@ -101,7 +101,7 @@ describe('Execution Metadata functions', () => {
|
||||||
} as IRunExecutionData;
|
} as IRunExecutionData;
|
||||||
|
|
||||||
expect(() => setWorkflowExecutionMetadata(executionData, 'te$t1$', 1234)).toThrow(
|
expect(() => setWorkflowExecutionMetadata(executionData, 'te$t1$', 1234)).toThrow(
|
||||||
ExecutionMetadataValidationError,
|
InvalidExecutionMetadataError,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(metadata).not.toEqual({
|
expect(metadata).not.toEqual({
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as Logger from './LoggerProxy';
|
import * as Logger from './LoggerProxy';
|
||||||
import { ReportableError, type ReportingOptions } from './errors/reportable.error';
|
import { ApplicationError, type ReportingOptions } from './errors/application.error';
|
||||||
|
|
||||||
interface ErrorReporter {
|
interface ErrorReporter {
|
||||||
report: (error: Error | string, options?: ReportingOptions) => void;
|
report: (error: Error | string, options?: ReportingOptions) => void;
|
||||||
|
@ -10,7 +10,7 @@ const instance: ErrorReporter = {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
let e = error;
|
let e = error;
|
||||||
do {
|
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);
|
Logger.error(`${e.constructor.name}: ${e.message}`, meta);
|
||||||
e = e.cause as Error;
|
e = e.cause as Error;
|
||||||
} while (e);
|
} while (e);
|
||||||
|
|
|
@ -15,7 +15,8 @@ import type {
|
||||||
NodeParameterValueType,
|
NodeParameterValueType,
|
||||||
WorkflowExecuteMode,
|
WorkflowExecuteMode,
|
||||||
} from './Interfaces';
|
} 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 { WorkflowDataProxy } from './WorkflowDataProxy';
|
||||||
import type { Workflow } from './Workflow';
|
import type { Workflow } from './Workflow';
|
||||||
|
|
||||||
|
|
|
@ -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 type { ExtensionMap } from './Extensions';
|
||||||
import { compact as oCompact } from './ObjectExtensions';
|
import { compact as oCompact } from './ObjectExtensions';
|
||||||
import deepEqual from 'deep-equal';
|
import deepEqual from 'deep-equal';
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ExpressionExtensionError } from './../ExpressionError';
|
import { ExpressionExtensionError } from '../errors/expression-extension.error';
|
||||||
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import type {
|
import type {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { ExpressionExtensionError } from '../ExpressionError';
|
import { ExpressionExtensionError } from '../errors/expression-extension.error';
|
||||||
import { parse, visit, types, print } from 'recast';
|
import { parse, visit, types, print } from 'recast';
|
||||||
import { getOption } from 'recast/lib/util';
|
import { getOption } from 'recast/lib/util';
|
||||||
import type { Config as EsprimaConfig } from 'esprima-next';
|
import type { Config as EsprimaConfig } from 'esprima-next';
|
||||||
|
|
|
@ -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';
|
import { average as aAverage } from './ArrayExtensions';
|
||||||
|
|
||||||
const min = Math.min;
|
const min = Math.min;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* @jest-environment jsdom
|
* @jest-environment jsdom
|
||||||
*/
|
*/
|
||||||
import { ExpressionExtensionError } from './../ExpressionError';
|
import { ExpressionExtensionError } from '../errors/expression-extension.error';
|
||||||
import type { ExtensionMap } from './Extensions';
|
import type { ExtensionMap } from './Extensions';
|
||||||
|
|
||||||
function format(value: number, extraArgs: unknown[]): string {
|
function format(value: number, extraArgs: unknown[]): string {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ExpressionExtensionError } from '../ExpressionError';
|
import { ExpressionExtensionError } from '../errors/expression-extension.error';
|
||||||
import type { ExtensionMap } from './Extensions';
|
import type { ExtensionMap } from './Extensions';
|
||||||
|
|
||||||
function isEmpty(value: object): boolean {
|
function isEmpty(value: object): boolean {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
// import { createHash } from 'crypto';
|
// import { createHash } from 'crypto';
|
||||||
import { titleCase } from 'title-case';
|
import { titleCase } from 'title-case';
|
||||||
import * as ExpressionError from '../ExpressionError';
|
|
||||||
import type { ExtensionMap } from './Extensions';
|
import type { ExtensionMap } from './Extensions';
|
||||||
import CryptoJS from 'crypto-js';
|
import CryptoJS from 'crypto-js';
|
||||||
import { encode } from 'js-base64';
|
import { encode } from 'js-base64';
|
||||||
import { transliterate } from 'transliteration';
|
import { transliterate } from 'transliteration';
|
||||||
|
import { ExpressionExtensionError } from '../errors/expression-extension.error';
|
||||||
|
|
||||||
const hashFunctions: Record<string, typeof CryptoJS.MD5> = {
|
const hashFunctions: Record<string, typeof CryptoJS.MD5> = {
|
||||||
md5: CryptoJS.MD5,
|
md5: CryptoJS.MD5,
|
||||||
|
@ -122,7 +122,7 @@ function hash(value: string, extraArgs?: unknown): string {
|
||||||
}
|
}
|
||||||
const hashFunction = hashFunctions[algorithm.toLowerCase()];
|
const hashFunction = hashFunctions[algorithm.toLowerCase()];
|
||||||
if (!hashFunction) {
|
if (!hashFunction) {
|
||||||
throw new ExpressionError.ExpressionExtensionError(
|
throw new ExpressionExtensionError(
|
||||||
`Unknown algorithm ${algorithm}. Available algorithms are: ${Object.keys(hashFunctions)
|
`Unknown algorithm ${algorithm}. Available algorithms are: ${Object.keys(hashFunctions)
|
||||||
.map((s) => s.toUpperCase())
|
.map((s) => s.toUpperCase())
|
||||||
.join(', ')}, and Base64.`,
|
.join(', ')}, and Base64.`,
|
||||||
|
@ -194,7 +194,7 @@ function toDate(value: string): Date {
|
||||||
const date = new Date(Date.parse(value));
|
const date = new Date(Date.parse(value));
|
||||||
|
|
||||||
if (date.toString() === 'Invalid Date') {
|
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 time component is not specified, force 00:00h
|
||||||
if (!/:/.test(value)) {
|
if (!/:/.test(value)) {
|
||||||
|
@ -224,7 +224,7 @@ function toInt(value: string, extraArgs: Array<number | undefined>) {
|
||||||
const int = parseInt(value.replace(CURRENCY_REGEXP, ''), radix);
|
const int = parseInt(value.replace(CURRENCY_REGEXP, ''), radix);
|
||||||
|
|
||||||
if (isNaN(int)) {
|
if (isNaN(int)) {
|
||||||
throw new ExpressionError.ExpressionExtensionError('cannot convert to integer');
|
throw new ExpressionExtensionError('cannot convert to integer');
|
||||||
}
|
}
|
||||||
|
|
||||||
return int;
|
return int;
|
||||||
|
@ -232,15 +232,13 @@ function toInt(value: string, extraArgs: Array<number | undefined>) {
|
||||||
|
|
||||||
function toFloat(value: string) {
|
function toFloat(value: string) {
|
||||||
if (value.includes(',')) {
|
if (value.includes(',')) {
|
||||||
throw new ExpressionError.ExpressionExtensionError(
|
throw new ExpressionExtensionError('cannot convert to float, expected . as decimal separator');
|
||||||
'cannot convert to float, expected . as decimal separator',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const float = parseFloat(value.replace(CURRENCY_REGEXP, ''));
|
const float = parseFloat(value.replace(CURRENCY_REGEXP, ''));
|
||||||
|
|
||||||
if (isNaN(float)) {
|
if (isNaN(float)) {
|
||||||
throw new ExpressionError.ExpressionExtensionError('cannot convert to float');
|
throw new ExpressionExtensionError('cannot convert to float');
|
||||||
}
|
}
|
||||||
|
|
||||||
return float;
|
return float;
|
||||||
|
|
|
@ -13,12 +13,13 @@ import type { AuthenticationMethod } from './Authentication';
|
||||||
import type { CODE_EXECUTION_MODES, CODE_LANGUAGES, LOG_LEVELS } from './Constants';
|
import type { CODE_EXECUTION_MODES, CODE_LANGUAGES, LOG_LEVELS } from './Constants';
|
||||||
import type { IDeferredPromise } from './DeferredPromise';
|
import type { IDeferredPromise } from './DeferredPromise';
|
||||||
import type { ExecutionStatus } from './ExecutionStatus';
|
import type { ExecutionStatus } from './ExecutionStatus';
|
||||||
import type { ExpressionError } from './ExpressionError';
|
import type { ExpressionError } from './errors/expression.error';
|
||||||
import type { NodeApiError, NodeOperationError } from './NodeErrors';
|
|
||||||
import type { Workflow } from './Workflow';
|
import type { Workflow } from './Workflow';
|
||||||
import type { WorkflowActivationError } from './WorkflowActivationError';
|
import type { WorkflowActivationError } from './errors/workflow-activation.error';
|
||||||
import type { WorkflowOperationError } from './WorkflowErrors';
|
import type { WorkflowOperationError } from './errors/workflow-operation.error';
|
||||||
import type { WorkflowHooks } from './WorkflowHooks';
|
import type { WorkflowHooks } from './WorkflowHooks';
|
||||||
|
import type { NodeOperationError } from './errors/node-operation.error';
|
||||||
|
import type { NodeApiError } from './errors/node-api.error';
|
||||||
|
|
||||||
export interface IAdditionalCredentialOptions {
|
export interface IAdditionalCredentialOptions {
|
||||||
oauth2?: IOAuth2Options;
|
oauth2?: IOAuth2Options;
|
||||||
|
@ -2373,3 +2374,5 @@ export type BannerName =
|
||||||
| 'TRIAL'
|
| 'TRIAL'
|
||||||
| 'NON_PRODUCTION_LICENSE'
|
| 'NON_PRODUCTION_LICENSE'
|
||||||
| 'EMAIL_CONFIRMATION';
|
| 'EMAIL_CONFIRMATION';
|
||||||
|
|
||||||
|
export type Severity = 'warning' | 'error';
|
||||||
|
|
|
@ -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 });
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
|
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
|
||||||
|
@ -37,11 +38,14 @@ import type {
|
||||||
PostReceiveAction,
|
PostReceiveAction,
|
||||||
JsonObject,
|
JsonObject,
|
||||||
} from './Interfaces';
|
} from './Interfaces';
|
||||||
import type { NodeError } from './NodeErrors';
|
|
||||||
import { NodeApiError, NodeOperationError } from './NodeErrors';
|
|
||||||
import * as NodeHelpers from './NodeHelpers';
|
import * as NodeHelpers from './NodeHelpers';
|
||||||
|
|
||||||
import type { Workflow } from './Workflow';
|
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 {
|
export class RoutingNode {
|
||||||
additionalData: IWorkflowExecuteAdditionalData;
|
additionalData: IWorkflowExecuteAdditionalData;
|
||||||
|
|
|
@ -24,7 +24,7 @@ import type {
|
||||||
ProxyInput,
|
ProxyInput,
|
||||||
} from './Interfaces';
|
} from './Interfaces';
|
||||||
import * as NodeHelpers from './NodeHelpers';
|
import * as NodeHelpers from './NodeHelpers';
|
||||||
import { ExpressionError } from './ExpressionError';
|
import { ExpressionError } from './errors/expression.error';
|
||||||
import type { Workflow } from './Workflow';
|
import type { Workflow } from './Workflow';
|
||||||
import { augmentArray, augmentObject } from './AugmentObject';
|
import { augmentArray, augmentObject } from './AugmentObject';
|
||||||
import { deepCopy } from './utils';
|
import { deepCopy } from './utils';
|
||||||
|
|
|
@ -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 {}
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @tech_debt Ensure `cause` can only be `Error` or `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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
168
packages/workflow/src/errors/abstract/node.error.ts
Normal file
168
packages/workflow/src/errors/abstract/node.error.ts
Normal file
|
@ -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}` : ''}`;
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,16 +6,17 @@ export type ReportingOptions = {
|
||||||
level?: Level;
|
level?: Level;
|
||||||
} & Pick<Event, 'tags' | 'extra'>;
|
} & Pick<Event, 'tags' | 'extra'>;
|
||||||
|
|
||||||
export type ReportableErrorOptions = Partial<ErrorOptions> & ReportingOptions;
|
export class ApplicationError extends Error {
|
||||||
|
|
||||||
export class ReportableError extends Error {
|
|
||||||
readonly level: Level;
|
readonly level: Level;
|
||||||
|
|
||||||
readonly tags?: Event['tags'];
|
readonly tags?: Event['tags'];
|
||||||
|
|
||||||
readonly extra?: Event['extra'];
|
readonly extra?: Event['extra'];
|
||||||
|
|
||||||
constructor(message: string, { level, tags, extra, ...rest }: ReportableErrorOptions) {
|
constructor(
|
||||||
|
message: string,
|
||||||
|
{ level, tags, extra, ...rest }: Partial<ErrorOptions> & ReportingOptions = {},
|
||||||
|
) {
|
||||||
super(message, rest);
|
super(message, rest);
|
||||||
this.level = level ?? 'error';
|
this.level = level ?? 'error';
|
||||||
this.tags = tags;
|
this.tags = tags;
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { SubworkflowOperationError } from './subworkflow-operation.error';
|
||||||
|
|
||||||
|
export class CliWorkflowOperationError extends SubworkflowOperationError {}
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { ExpressionError } from './expression.error';
|
||||||
|
|
||||||
|
export class ExpressionExtensionError extends ExpressionError {}
|
|
@ -1,5 +1,5 @@
|
||||||
import type { IDataObject } from './Interfaces';
|
import type { IDataObject } from '../Interfaces';
|
||||||
import { ExecutionBaseError } from './NodeErrors';
|
import { ExecutionBaseError } from './abstract/execution-base.error';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for instantiating an expression error
|
* Class for instantiating an expression error
|
||||||
|
@ -47,5 +47,3 @@ export class ExpressionError extends ExecutionBaseError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ExpressionExtensionError extends ExpressionError {}
|
|
|
@ -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 { WebhookPathTakenError } 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';
|
||||||
|
|
267
packages/workflow/src/errors/node-api.error.ts
Normal file
267
packages/workflow/src/errors/node-api.error.ts
Normal file
|
@ -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}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
34
packages/workflow/src/errors/node-operation.error.ts
Normal file
34
packages/workflow/src/errors/node-operation.error.ts
Normal file
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
7
packages/workflow/src/errors/node-ssl.error.ts
Normal file
7
packages/workflow/src/errors/node-ssl.error.ts
Normal file
|
@ -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 });
|
||||||
|
}
|
||||||
|
}
|
18
packages/workflow/src/errors/subworkflow-operation.error.ts
Normal file
18
packages/workflow/src/errors/subworkflow-operation.error.ts
Normal file
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
10
packages/workflow/src/errors/webhook-taken.error.ts
Normal file
10
packages/workflow/src/errors/webhook-taken.error.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { WorkflowActivationError } from './workflow-activation.error';
|
||||||
|
|
||||||
|
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.`,
|
||||||
|
{ severity: 'warning', cause },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import type { INode } from './Interfaces';
|
import type { INode, Severity } from '../Interfaces';
|
||||||
import { ExecutionBaseError, type Severity } from './NodeErrors';
|
import { ExecutionBaseError } from './abstract/execution-base.error';
|
||||||
|
|
||||||
interface WorkflowActivationErrorOptions {
|
interface WorkflowActivationErrorOptions {
|
||||||
cause?: Error;
|
cause?: Error;
|
||||||
|
@ -34,14 +34,3 @@ export class WorkflowActivationError extends ExecutionBaseError {
|
||||||
if (severity) this.severity = severity;
|
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 },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { WorkflowActivationError } from './workflow-activation.error';
|
||||||
|
|
||||||
|
export class WorkflowDeactivationError extends WorkflowActivationError {}
|
23
packages/workflow/src/errors/workflow-operation.error.ts
Normal file
23
packages/workflow/src/errors/workflow-operation.error.ts
Normal file
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,14 +15,10 @@ export * from './Interfaces';
|
||||||
export * from './MessageEventBus';
|
export * from './MessageEventBus';
|
||||||
export * from './ExecutionStatus';
|
export * from './ExecutionStatus';
|
||||||
export * from './Expression';
|
export * from './Expression';
|
||||||
export * from './ExpressionError';
|
|
||||||
export * from './NodeErrors';
|
|
||||||
export * from './NodeHelpers';
|
export * from './NodeHelpers';
|
||||||
export * from './RoutingNode';
|
export * from './RoutingNode';
|
||||||
export * from './Workflow';
|
export * from './Workflow';
|
||||||
export * from './WorkflowActivationError';
|
|
||||||
export * from './WorkflowDataProxy';
|
export * from './WorkflowDataProxy';
|
||||||
export * from './WorkflowErrors';
|
|
||||||
export * from './WorkflowHooks';
|
export * from './WorkflowHooks';
|
||||||
export * from './VersionedNodeType';
|
export * from './VersionedNodeType';
|
||||||
export { LoggerProxy, NodeHelpers, ObservableObject, TelemetryHelpers };
|
export { LoggerProxy, NodeHelpers, ObservableObject, TelemetryHelpers };
|
||||||
|
|
|
@ -9,7 +9,7 @@ import type { ExpressionTestEvaluation, ExpressionTestTransform } from './Expres
|
||||||
import { baseFixtures } from './ExpressionFixtures/base';
|
import { baseFixtures } from './ExpressionFixtures/base';
|
||||||
import type { INodeExecutionData } from '@/Interfaces';
|
import type { INodeExecutionData } from '@/Interfaces';
|
||||||
import { extendSyntax } from '@/Extensions/ExpressionExtension';
|
import { extendSyntax } from '@/Extensions/ExpressionExtension';
|
||||||
import { ExpressionError } from '@/ExpressionError';
|
import { ExpressionError } from '@/errors/expression.error';
|
||||||
import { setDifferEnabled, setEvaluator } from '@/ExpressionEvaluatorProxy';
|
import { setDifferEnabled, setEvaluator } from '@/ExpressionEvaluatorProxy';
|
||||||
|
|
||||||
setDifferEnabled(true);
|
setDifferEnabled(true);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type { INode } from '@/Interfaces';
|
import type { INode } from '@/Interfaces';
|
||||||
import { NodeApiError, NodeOperationError } from '@/NodeErrors';
|
import { NodeOperationError } from '@/errors';
|
||||||
|
import { NodeApiError } from '@/errors/node-api.error';
|
||||||
|
|
||||||
const node: INode = {
|
const node: INode = {
|
||||||
id: '1',
|
id: '1',
|
||||||
|
|
|
@ -2,7 +2,7 @@ import type { IConnections, IExecuteData, INode, IRunExecutionData } from '@/Int
|
||||||
import { Workflow } from '@/Workflow';
|
import { Workflow } from '@/Workflow';
|
||||||
import { WorkflowDataProxy } from '@/WorkflowDataProxy';
|
import { WorkflowDataProxy } from '@/WorkflowDataProxy';
|
||||||
import * as Helpers from './Helpers';
|
import * as Helpers from './Helpers';
|
||||||
import { ExpressionError } from '@/ExpressionError';
|
import { ExpressionError } from '@/errors/expression.error';
|
||||||
|
|
||||||
describe('WorkflowDataProxy', () => {
|
describe('WorkflowDataProxy', () => {
|
||||||
describe('test data proxy', () => {
|
describe('test data proxy', () => {
|
||||||
|
|
Loading…
Reference in a new issue