mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
refactor(core): Switch plain errors in workflow
to ApplicationError
(no-changelog) (#7877)
Ensure all errors in `workflow` are `ApplicationError` or children of it and contain no variables in the message, to continue normalizing all the errors we report to Sentry Follow-up to: https://github.com/n8n-io/n8n/pull/7873
This commit is contained in:
parent
ce2d388f05
commit
67702c2485
|
@ -24,7 +24,7 @@ export function init(errorReporter: ErrorReporter) {
|
|||
|
||||
const wrap = (e: unknown) => {
|
||||
if (e instanceof Error) return e;
|
||||
if (typeof e === 'string') return new Error(e);
|
||||
if (typeof e === 'string') return new ApplicationError(e);
|
||||
return;
|
||||
};
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import { extendedFunctions } from './Extensions/ExtendedFunctions';
|
|||
import { extendSyntax } from './Extensions/ExpressionExtension';
|
||||
import { evaluateExpression, setErrorHandler } from './ExpressionEvaluatorProxy';
|
||||
import { getGlobalState } from './GlobalState';
|
||||
import { ApplicationError } from './errors/application.error';
|
||||
|
||||
const IS_FRONTEND_IN_DEV_MODE =
|
||||
typeof process === 'object' &&
|
||||
|
@ -75,7 +76,7 @@ export class Expression {
|
|||
const typeName = Array.isArray(value) ? 'Array' : 'Object';
|
||||
|
||||
if (value instanceof DateTime && value.invalidReason !== null) {
|
||||
throw new Error('invalid DateTime');
|
||||
throw new ApplicationError('invalid DateTime');
|
||||
}
|
||||
|
||||
let result = '';
|
||||
|
@ -310,12 +311,12 @@ export class Expression {
|
|||
const extendedExpression = extendSyntax(parameterValue);
|
||||
const returnValue = this.renderExpression(extendedExpression, data);
|
||||
if (typeof returnValue === 'function') {
|
||||
if (returnValue.name === '$') throw new Error('invalid syntax');
|
||||
if (returnValue.name === '$') throw new ApplicationError('invalid syntax');
|
||||
|
||||
if (returnValue.name === 'DateTime')
|
||||
throw new Error('this is a DateTime, please access its methods');
|
||||
throw new ApplicationError('this is a DateTime, please access its methods');
|
||||
|
||||
throw new Error('this is a function, please add ()');
|
||||
throw new ApplicationError('this is a function, please add ()');
|
||||
} else if (typeof returnValue === 'string') {
|
||||
return returnValue;
|
||||
} else if (returnValue !== null && typeof returnValue === 'object') {
|
||||
|
@ -339,14 +340,14 @@ export class Expression {
|
|||
} catch (error) {
|
||||
if (isExpressionError(error)) throw error;
|
||||
|
||||
if (isSyntaxError(error)) throw new Error('invalid syntax');
|
||||
if (isSyntaxError(error)) throw new ApplicationError('invalid syntax');
|
||||
|
||||
if (isTypeError(error) && IS_FRONTEND && error.message.endsWith('is not a function')) {
|
||||
const match = error.message.match(/(?<msg>[^.]+is not a function)/);
|
||||
|
||||
if (!match?.groups?.msg) return null;
|
||||
|
||||
throw new Error(match.groups.msg);
|
||||
throw new ApplicationError(match.groups.msg);
|
||||
}
|
||||
} finally {
|
||||
Object.defineProperty(Function.prototype, 'constructor', { value: fnConstructors.sync });
|
||||
|
|
|
@ -48,6 +48,7 @@ import { deepCopy } from './utils';
|
|||
|
||||
import { DateTime } from 'luxon';
|
||||
import type { Workflow } from './Workflow';
|
||||
import { ApplicationError } from './errors/application.error';
|
||||
|
||||
export const cronNodeOptions: INodePropertyCollection[] = [
|
||||
{
|
||||
|
@ -416,7 +417,7 @@ export function getContext(
|
|||
): IContextObject {
|
||||
if (runExecutionData.executionData === undefined) {
|
||||
// TODO: Should not happen leave it for test now
|
||||
throw new Error('The "executionData" is not initialized!');
|
||||
throw new ApplicationError('`executionData` is not initialized');
|
||||
}
|
||||
|
||||
let key: string;
|
||||
|
@ -424,11 +425,16 @@ export function getContext(
|
|||
key = 'flow';
|
||||
} else if (type === 'node') {
|
||||
if (node === undefined) {
|
||||
throw new Error('The request data of context type "node" the node parameter has to be set!');
|
||||
// @TODO: What does this mean?
|
||||
throw new ApplicationError(
|
||||
'The request data of context type "node" the node parameter has to be set!',
|
||||
);
|
||||
}
|
||||
key = `node:${node.name}`;
|
||||
} else {
|
||||
throw new Error(`The context type "${type}" is not know. Only "flow" and node" are supported!`);
|
||||
throw new ApplicationError('Unknown context type. Only `flow` and `node` are supported.', {
|
||||
extra: { contextType: type },
|
||||
});
|
||||
}
|
||||
|
||||
if (runExecutionData.executionData.contextData[key] === undefined) {
|
||||
|
@ -530,7 +536,7 @@ export function getParameterResolveOrder(
|
|||
}
|
||||
|
||||
if (iterations > lastIndexReduction + nodePropertiesArray.length) {
|
||||
throw new Error(
|
||||
throw new ApplicationError(
|
||||
'Could not resolve parameter dependencies. Max iterations reached! Hint: If `displayOptions` are specified in any child parameter of a parent `collection` or `fixedCollection`, remove the `displayOptions` from the child parameter.',
|
||||
);
|
||||
}
|
||||
|
@ -773,9 +779,9 @@ export function getNodeParameters(
|
|||
) as INodePropertyCollection;
|
||||
|
||||
if (nodePropertyOptions === undefined) {
|
||||
throw new Error(
|
||||
`Could not find property option "${itemName}" for "${nodeProperties.name}"`,
|
||||
);
|
||||
throw new ApplicationError('Could not find property option', {
|
||||
extra: { propertyOption: itemName, property: nodeProperties.name },
|
||||
});
|
||||
}
|
||||
|
||||
tempNodePropertiesArray = nodePropertyOptions.values!;
|
||||
|
@ -1054,7 +1060,9 @@ export function getNodeInputs(
|
|||
{},
|
||||
) || []) as ConnectionTypes[];
|
||||
} catch (e) {
|
||||
throw new Error(`Could not calculate inputs dynamically for node "${node.name}"`);
|
||||
throw new ApplicationError('Could not calculate inputs dynamically for node', {
|
||||
extra: { nodeName: node.name },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1077,7 +1085,9 @@ export function getNodeOutputs(
|
|||
{},
|
||||
) || []) as ConnectionTypes[];
|
||||
} catch (e) {
|
||||
throw new Error(`Could not calculate outputs dynamically for node "${node.name}"`);
|
||||
throw new ApplicationError('Could not calculate outputs dynamically for node', {
|
||||
extra: { nodeName: node.name },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1258,7 +1268,7 @@ export const tryToParseNumber = (value: unknown): number => {
|
|||
const isValidNumber = !isNaN(Number(value));
|
||||
|
||||
if (!isValidNumber) {
|
||||
throw new Error(`Could not parse '${String(value)}' to number.`);
|
||||
throw new ApplicationError('Failed to parse value to number', { extra: { value } });
|
||||
}
|
||||
return Number(value);
|
||||
};
|
||||
|
@ -1282,7 +1292,9 @@ export const tryToParseBoolean = (value: unknown): value is boolean => {
|
|||
}
|
||||
}
|
||||
|
||||
throw new Error(`Could not parse '${String(value)}' to boolean.`);
|
||||
throw new ApplicationError('Failed to parse value as boolean', {
|
||||
extra: { value },
|
||||
});
|
||||
};
|
||||
|
||||
export const tryToParseDateTime = (value: unknown): DateTime => {
|
||||
|
@ -1306,7 +1318,7 @@ export const tryToParseDateTime = (value: unknown): DateTime => {
|
|||
return sqlDate;
|
||||
}
|
||||
|
||||
throw new Error(`The value "${dateString}" is not a valid date.`);
|
||||
throw new ApplicationError('Value is not a valid date', { extra: { dateString } });
|
||||
};
|
||||
|
||||
export const tryToParseTime = (value: unknown): string => {
|
||||
|
@ -1314,7 +1326,7 @@ export const tryToParseTime = (value: unknown): string => {
|
|||
String(value),
|
||||
);
|
||||
if (!isTimeInput) {
|
||||
throw new Error(`The value "${String(value)}" is not a valid time.`);
|
||||
throw new ApplicationError('Value is not a valid time', { extra: { value } });
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
|
@ -1333,11 +1345,11 @@ export const tryToParseArray = (value: unknown): unknown[] => {
|
|||
}
|
||||
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error(`The value "${String(value)}" is not a valid array.`);
|
||||
throw new ApplicationError('Value is not a valid array', { extra: { value } });
|
||||
}
|
||||
return parsed;
|
||||
} catch (e) {
|
||||
throw new Error(`The value "${String(value)}" is not a valid array.`);
|
||||
throw new ApplicationError('Value is not a valid array', { extra: { value } });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1348,11 +1360,11 @@ export const tryToParseObject = (value: unknown): object => {
|
|||
try {
|
||||
const o = JSON.parse(String(value));
|
||||
if (typeof o !== 'object' || Array.isArray(o)) {
|
||||
throw new Error(`The value "${String(value)}" is not a valid object.`);
|
||||
throw new ApplicationError('Value is not a valid object', { extra: { value } });
|
||||
}
|
||||
return o;
|
||||
} catch (e) {
|
||||
throw new Error(`The value "${String(value)}" is not a valid object.`);
|
||||
throw new ApplicationError('Value is not a valid object', { extra: { value } });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import type {
|
|||
INodeTypes,
|
||||
INodeType,
|
||||
} from './Interfaces';
|
||||
import { ApplicationError } from './errors/application.error';
|
||||
|
||||
const STICKY_NODE_TYPE = 'n8n-nodes-base.stickyNote';
|
||||
|
||||
|
@ -95,7 +96,7 @@ export function getDomainPath(raw: string, urlParts = URL_PARTS_REGEX): string {
|
|||
try {
|
||||
const url = new URL(raw);
|
||||
|
||||
if (!url.hostname) throw new Error('Malformed URL');
|
||||
if (!url.hostname) throw new ApplicationError('Malformed URL');
|
||||
|
||||
return sanitizeRoute(url.pathname);
|
||||
} catch {
|
||||
|
|
|
@ -51,6 +51,7 @@ import * as ObservableObject from './ObservableObject';
|
|||
import { RoutingNode } from './RoutingNode';
|
||||
import { Expression } from './Expression';
|
||||
import { NODES_WITH_RENAMABLE_CONTENT } from './Constants';
|
||||
import { ApplicationError } from './errors/application.error';
|
||||
|
||||
function dedupe<T>(arr: T[]): T[] {
|
||||
return [...new Set(arr)];
|
||||
|
@ -113,7 +114,10 @@ export class Workflow {
|
|||
// expression resolution also then when the unknown node
|
||||
// does not get used.
|
||||
continue;
|
||||
// throw new Error(`The node type "${node.type}" of node "${node.name}" is not known.`);
|
||||
// throw new ApplicationError(`Node with unknown node type`, {
|
||||
// tags: { nodeType: node.type },
|
||||
// extra: { node },
|
||||
// });
|
||||
}
|
||||
|
||||
// Add default values
|
||||
|
@ -312,15 +316,15 @@ export class Workflow {
|
|||
key = 'global';
|
||||
} else if (type === 'node') {
|
||||
if (node === undefined) {
|
||||
throw new Error(
|
||||
throw new ApplicationError(
|
||||
'The request data of context type "node" the node parameter has to be set!',
|
||||
);
|
||||
}
|
||||
key = `node:${node.name}`;
|
||||
} else {
|
||||
throw new Error(
|
||||
`The context type "${type}" is not know. Only "global" and node" are supported!`,
|
||||
);
|
||||
throw new ApplicationError('Unknown context type. Only `global` and `node` are supported.', {
|
||||
extra: { contextType: type },
|
||||
});
|
||||
}
|
||||
|
||||
if (this.staticData[key] === undefined) {
|
||||
|
@ -1092,13 +1096,17 @@ export class Workflow {
|
|||
const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
|
||||
|
||||
if (nodeType === undefined) {
|
||||
throw new Error(`The node type "${node.type}" of node "${node.name}" is not known.`);
|
||||
throw new ApplicationError('Node with unknown node type', {
|
||||
extra: { nodeName: node.name },
|
||||
tags: { nodeType: node.type },
|
||||
});
|
||||
}
|
||||
|
||||
if (!nodeType.trigger) {
|
||||
throw new Error(
|
||||
`The node type "${node.type}" of node "${node.name}" does not have a trigger function defined.`,
|
||||
);
|
||||
throw new ApplicationError('Node type does not have a trigger function defined', {
|
||||
extra: { nodeName: node.name },
|
||||
tags: { nodeType: node.type },
|
||||
});
|
||||
}
|
||||
|
||||
if (mode === 'manual') {
|
||||
|
@ -1169,13 +1177,17 @@ export class Workflow {
|
|||
const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
|
||||
|
||||
if (nodeType === undefined) {
|
||||
throw new Error(`The node type "${node.type}" of node "${node.name}" is not known.`);
|
||||
throw new ApplicationError('Node with unknown node type', {
|
||||
extra: { nodeName: node.name },
|
||||
tags: { nodeType: node.type },
|
||||
});
|
||||
}
|
||||
|
||||
if (!nodeType.poll) {
|
||||
throw new Error(
|
||||
`The node type "${node.type}" of node "${node.name}" does not have a poll function defined.`,
|
||||
);
|
||||
throw new ApplicationError('Node type does not have a poll function defined', {
|
||||
extra: { nodeName: node.name },
|
||||
tags: { nodeType: node.type },
|
||||
});
|
||||
}
|
||||
|
||||
return nodeType.poll.call(pollFunctions);
|
||||
|
@ -1195,9 +1207,13 @@ export class Workflow {
|
|||
): Promise<IWebhookResponseData> {
|
||||
const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
|
||||
if (nodeType === undefined) {
|
||||
throw new Error(`The type of the webhook node "${node.name}" is not known.`);
|
||||
throw new ApplicationError('Unknown node type of webhook node', {
|
||||
extra: { nodeName: node.name },
|
||||
});
|
||||
} else if (nodeType.webhook === undefined) {
|
||||
throw new Error(`The node "${node.name}" does not have any webhooks defined.`);
|
||||
throw new ApplicationError('Node does not have any webhooks defined', {
|
||||
extra: { nodeName: node.name },
|
||||
});
|
||||
}
|
||||
|
||||
const context = nodeExecuteFunctions.getExecuteWebhookFunctions(
|
||||
|
@ -1241,7 +1257,9 @@ export class Workflow {
|
|||
|
||||
const nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
|
||||
if (nodeType === undefined) {
|
||||
throw new Error(`Node type "${node.type}" is not known so can not run it!`);
|
||||
throw new ApplicationError('Node type is unknown so cannot run it', {
|
||||
tags: { nodeType: node.type },
|
||||
});
|
||||
}
|
||||
|
||||
let connectionInputData: INodeExecutionData[] = [];
|
||||
|
|
|
@ -29,6 +29,7 @@ import type { Workflow } from './Workflow';
|
|||
import { augmentArray, augmentObject } from './AugmentObject';
|
||||
import { deepCopy } from './utils';
|
||||
import { getGlobalState } from './GlobalState';
|
||||
import { ApplicationError } from './errors/application.error';
|
||||
|
||||
export function isResourceLocatorValue(value: unknown): value is INodeParameterResourceLocator {
|
||||
return Boolean(
|
||||
|
@ -190,7 +191,9 @@ export class WorkflowDataProxy {
|
|||
if (name[0] === '&') {
|
||||
const key = name.slice(1);
|
||||
if (!that.siblingParameters.hasOwnProperty(key)) {
|
||||
throw new Error(`Could not find sibling parameter "${key}" on node "${nodeName}"`);
|
||||
throw new ApplicationError('Could not find sibling parameter on node', {
|
||||
extra: { nodeName, parameter: key },
|
||||
});
|
||||
}
|
||||
returnValue = that.siblingParameters[key];
|
||||
} else {
|
||||
|
@ -300,8 +303,8 @@ export class WorkflowDataProxy {
|
|||
const taskData = that.runExecutionData.resultData.runData[nodeName][runIndex].data!;
|
||||
|
||||
if (!taskData.main?.length || taskData.main[0] === null) {
|
||||
// throw new Error(`No data found for item-index: "${itemIndex}"`);
|
||||
throw new ExpressionError('No data found from "main" input.', {
|
||||
// throw new ApplicationError('No data found for item-index', { extra: { itemIndex } });
|
||||
throw new ExpressionError('No data found from `main` input', {
|
||||
runIndex: that.runIndex,
|
||||
itemIndex: that.itemIndex,
|
||||
});
|
||||
|
@ -765,7 +768,7 @@ export class WorkflowDataProxy {
|
|||
if (itemInput >= taskData.source.length) {
|
||||
// `Could not resolve pairedItem as the defined node input '${itemInput}' does not exist on node '${sourceData!.previousNode}'.`
|
||||
// Actual error does not matter as it gets caught below and `null` will be returned
|
||||
throw new Error('Not found');
|
||||
throw new ApplicationError('Not found');
|
||||
}
|
||||
|
||||
return getPairedItem(destinationNodeName, taskData.source[itemInput], item);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import FormData from 'form-data';
|
||||
import type { BinaryFileType, JsonObject } from './Interfaces';
|
||||
import { ApplicationError } from './errors/application.error';
|
||||
|
||||
const readStreamClasses = new Set(['ReadStream', 'Readable', 'ReadableStream']);
|
||||
|
||||
|
@ -77,7 +78,7 @@ export const jsonParse = <T>(jsonString: string, options?: JSONParseOptions<T>):
|
|||
if (options?.fallbackValue !== undefined) {
|
||||
return options.fallbackValue;
|
||||
} else if (options?.errorMessage) {
|
||||
throw new Error(options.errorMessage);
|
||||
throw new ApplicationError(options.errorMessage);
|
||||
}
|
||||
|
||||
throw error;
|
||||
|
|
|
@ -39,6 +39,7 @@ import { WorkflowHooks } from '@/WorkflowHooks';
|
|||
import * as NodeHelpers from '@/NodeHelpers';
|
||||
import { deepCopy } from '@/utils';
|
||||
import { getGlobalState } from '@/GlobalState';
|
||||
import { ApplicationError } from '@/errors/application.error';
|
||||
|
||||
export interface INodeTypesObject {
|
||||
[key: string]: INodeType;
|
||||
|
@ -55,14 +56,14 @@ export class Credentials extends ICredentials {
|
|||
|
||||
getData(): ICredentialDataDecryptedObject {
|
||||
if (this.data === undefined) {
|
||||
throw new Error('No data is set so nothing can be returned.');
|
||||
throw new ApplicationError('No data is set so nothing can be returned');
|
||||
}
|
||||
return JSON.parse(this.data);
|
||||
}
|
||||
|
||||
getDataToSave(): ICredentialsEncrypted {
|
||||
if (this.data === undefined) {
|
||||
throw new Error('No credentials were set to save.');
|
||||
throw new ApplicationError('No credentials were set to save');
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -135,13 +136,15 @@ export function getNodeParameter(
|
|||
): NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[] | object {
|
||||
const nodeType = workflow.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
|
||||
if (nodeType === undefined) {
|
||||
throw new Error(`Node type "${node.type}" is not known so can not return parameter value!`);
|
||||
throw new ApplicationError('Node type is unknown so cannot return parameter value', {
|
||||
tags: { nodeType: node.type },
|
||||
});
|
||||
}
|
||||
|
||||
const value = get(node.parameters, parameterName, fallbackValue);
|
||||
|
||||
if (value === undefined) {
|
||||
throw new Error(`Could not get parameter "${parameterName}"!`);
|
||||
throw new ApplicationError('Could not get parameter', { extra: { parameterName } });
|
||||
}
|
||||
|
||||
let returnData;
|
||||
|
@ -211,12 +214,15 @@ export function getExecuteFunctions(
|
|||
}
|
||||
|
||||
if (inputData[inputName].length < inputIndex) {
|
||||
throw new Error(`Could not get input index "${inputIndex}" of input "${inputName}"!`);
|
||||
throw new ApplicationError('Could not get input index', {
|
||||
extra: { inputIndex, inputName },
|
||||
});
|
||||
}
|
||||
|
||||
if (inputData[inputName][inputIndex] === null) {
|
||||
// return [];
|
||||
throw new Error(`Value "${inputIndex}" of input "${inputName}" did not get set!`);
|
||||
throw new ApplicationError('Value of input did not get set', {
|
||||
extra: { inputIndex, inputName },
|
||||
});
|
||||
}
|
||||
|
||||
return inputData[inputName][inputIndex] as INodeExecutionData[];
|
||||
|
@ -387,21 +393,23 @@ export function getExecuteSingleFunctions(
|
|||
}
|
||||
|
||||
if (inputData[inputName].length < inputIndex) {
|
||||
throw new Error(`Could not get input index "${inputIndex}" of input "${inputName}"!`);
|
||||
throw new ApplicationError('Could not get input index', {
|
||||
extra: { inputIndex, inputName },
|
||||
});
|
||||
}
|
||||
|
||||
const allItems = inputData[inputName][inputIndex];
|
||||
|
||||
if (allItems === null) {
|
||||
// return [];
|
||||
throw new Error(`Value "${inputIndex}" of input "${inputName}" did not get set!`);
|
||||
throw new ApplicationError('Value of input did not get set', {
|
||||
extra: { inputIndex, inputName },
|
||||
});
|
||||
}
|
||||
|
||||
if (allItems[itemIndex] === null) {
|
||||
// return [];
|
||||
throw new Error(
|
||||
`Value "${inputIndex}" of input "${inputName}" with itemIndex "${itemIndex}" did not get set!`,
|
||||
);
|
||||
throw new ApplicationError('Value of input with item index did not get set', {
|
||||
extra: { inputIndex, inputName, itemIndex },
|
||||
});
|
||||
}
|
||||
|
||||
return allItems[itemIndex];
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { ApplicationError } from '@/errors/application.error';
|
||||
import { jsonParse, jsonStringify, deepCopy, isObjectEmpty, fileTypeFromMimeType } from '@/utils';
|
||||
|
||||
describe('isObjectEmpty', () => {
|
||||
|
@ -58,7 +59,11 @@ describe('isObjectEmpty', () => {
|
|||
const { calls } = keySpy.mock;
|
||||
|
||||
const assertCalls = (count: number) => {
|
||||
if (calls.length !== count) throw new Error(`Object.keys was called ${calls.length} times`);
|
||||
if (calls.length !== count) {
|
||||
throw new ApplicationError('`Object.keys()` was called an unexpected number of times', {
|
||||
extra: { times: calls.length },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
assertCalls(0);
|
||||
|
|
Loading…
Reference in a new issue