refactor(core): Switch plain errors in cli to ApplicationError (#7857)

Ensure all errors in `cli` 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/7839
This commit is contained in:
Iván Ovejero 2023-11-29 12:25:10 +01:00 committed by GitHub
parent 87def60979
commit c08c5cc37b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 277 additions and 195 deletions

View file

@ -7,7 +7,7 @@ import type {
IRun,
ExecutionStatus,
} from 'n8n-workflow';
import { WorkflowOperationError, createDeferredPromise } from 'n8n-workflow';
import { ApplicationError, WorkflowOperationError, createDeferredPromise } from 'n8n-workflow';
import type { ChildProcess } from 'child_process';
import type PCancelable from 'p-cancelable';
@ -64,7 +64,7 @@ export class ActiveExecutions {
await Container.get(ExecutionRepository).createNewExecution(fullExecutionData);
executionId = executionResult.id;
if (executionId === undefined) {
throw new Error('There was an issue assigning an execution id to the execution');
throw new ApplicationError('There was an issue assigning an execution id to the execution');
}
executionStatus = 'running';
} else {
@ -98,9 +98,9 @@ export class ActiveExecutions {
attachWorkflowExecution(executionId: string, workflowExecution: PCancelable<IRun>) {
if (this.activeExecutions[executionId] === undefined) {
throw new Error(
`No active execution with id "${executionId}" got found to attach to workflowExecution to!`,
);
throw new ApplicationError('No active execution found to attach to workflow execution to', {
extra: { executionId },
});
}
this.activeExecutions[executionId].workflowExecution = workflowExecution;
@ -111,9 +111,9 @@ export class ActiveExecutions {
responsePromise: IDeferredPromise<IExecuteResponsePromiseData>,
): void {
if (this.activeExecutions[executionId] === undefined) {
throw new Error(
`No active execution with id "${executionId}" got found to attach to workflowExecution to!`,
);
throw new ApplicationError('No active execution found to attach to workflow execution to', {
extra: { executionId },
});
}
this.activeExecutions[executionId].responsePromise = responsePromise;

View file

@ -6,7 +6,7 @@ import type {
WorkflowActivateMode,
WorkflowExecuteMode,
} from 'n8n-workflow';
import { WebhookPathTakenError } from 'n8n-workflow';
import { ApplicationError, WebhookPathTakenError } from 'n8n-workflow';
import * as NodeExecuteFunctions from 'n8n-core';
@Service()
@ -32,7 +32,9 @@ export class ActiveWebhooks {
activation: WorkflowActivateMode,
): Promise<void> {
if (workflow.id === undefined) {
throw new Error('Webhooks can only be added for saved workflows as an id is needed!');
throw new ApplicationError(
'Webhooks can only be added for saved workflows as an ID is needed',
);
}
if (webhookData.path.endsWith('/')) {
webhookData.path = webhookData.path.slice(0, -1);

View file

@ -30,6 +30,7 @@ import {
WorkflowActivationError,
ErrorReporterProxy as ErrorReporter,
WebhookPathTakenError,
ApplicationError,
} from 'n8n-workflow';
import type express from 'express';
@ -425,7 +426,7 @@ export class ActiveWorkflowRunner implements IWebhookManager {
});
if (workflowData === null) {
throw new Error(`Could not find workflow with id "${workflowId}"`);
throw new ApplicationError('Could not find workflow', { extra: { workflowId } });
}
const workflow = new Workflow({

View file

@ -1,6 +1,11 @@
import { Service } from 'typedi';
import { loadClassInIsolation } from 'n8n-core';
import type { ICredentialType, ICredentialTypes, LoadedClass } from 'n8n-workflow';
import {
ApplicationError,
type ICredentialType,
type ICredentialTypes,
type LoadedClass,
} from 'n8n-workflow';
import { RESPONSE_ERROR_MESSAGES } from '@/constants';
import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials';
@ -46,6 +51,8 @@ export class CredentialTypes implements ICredentialTypes {
loadedCredentials[type] = { sourcePath, type: loaded };
return loadedCredentials[type];
}
throw new Error(`${RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL}: ${type}`);
throw new ApplicationError(RESPONSE_ERROR_MESSAGES.NO_CREDENTIAL, {
tags: { credentialType: type },
});
}
}

View file

@ -40,6 +40,7 @@ import {
RoutingNode,
Workflow,
ErrorReporterProxy as ErrorReporter,
ApplicationError,
} from 'n8n-workflow';
import type { ICredentialsDb } from '@/Interfaces';
@ -81,7 +82,9 @@ const mockNodeTypes: INodeTypes = {
},
getByNameAndVersion(nodeType: string, version?: number): INodeType {
if (!mockNodesData[nodeType]) {
throw new Error(`${RESPONSE_ERROR_MESSAGES.NO_NODE}: ${nodeType}`);
throw new ApplicationError(RESPONSE_ERROR_MESSAGES.NO_NODE, {
tags: { nodeType },
});
}
return NodeHelpers.getVersionedNodeType(mockNodesData[nodeType].type, version);
},
@ -258,7 +261,10 @@ export class CredentialsHelper extends ICredentialsHelper {
userId?: string,
): Promise<Credentials> {
if (!nodeCredential.id) {
throw new Error(`Credential "${nodeCredential.name}" of type "${type}" has no ID.`);
throw new ApplicationError('Found credential with no ID.', {
extra: { credentialName: nodeCredential.name },
tags: { credentialType: type },
});
}
let credential: CredentialsEntity;
@ -291,7 +297,7 @@ export class CredentialsHelper extends ICredentialsHelper {
const credentialTypeData = this.credentialTypes.getByName(type);
if (credentialTypeData === undefined) {
throw new Error(`The credentials of type "${type}" are not known.`);
throw new ApplicationError('Unknown credential type', { tags: { credentialType: type } });
}
if (credentialTypeData.extends === undefined) {

View file

@ -3,7 +3,7 @@ import { Container } from 'typedi';
import type { DataSourceOptions as ConnectionOptions, EntityManager, LoggerOptions } from 'typeorm';
import { DataSource as Connection } from 'typeorm';
import type { TlsOptions } from 'tls';
import { ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
import { ApplicationError, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
import config from '@/config';
@ -93,7 +93,7 @@ export function getConnectionOptions(dbType: DatabaseType): ConnectionOptions {
return getSqliteConnectionOptions();
default:
throw new Error(`The database "${dbType}" is currently not supported!`);
throw new ApplicationError('Database type currently not supported', { extra: { dbType } });
}
}

View file

@ -10,6 +10,7 @@ import { UserRepository } from '@db/repositories/user.repository';
import { CredentialsRepository } from '@db/repositories/credentials.repository';
import { SettingsRepository } from '@db/repositories/settings.repository';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import { ApplicationError } from 'n8n-workflow';
@Service()
export class ExternalHooks implements IExternalHooksClass {
@ -71,12 +72,13 @@ export class ExternalHooks implements IExternalHooksClass {
const hookFile = require(hookFilePath) as IExternalHooksFileData;
this.loadHooks(hookFile);
} catch (error) {
throw new Error(
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
`Problem loading external hook file "${hookFilePath}": ${error.message}`,
{ cause: error as Error },
);
} catch (e) {
const error = e instanceof Error ? e : new Error(`${e}`);
throw new ApplicationError('Problem loading external hook file', {
extra: { errorMessage: error.message, hookFilePath },
cause: error,
});
}
}
}

View file

@ -10,7 +10,7 @@ import Container, { Service } from 'typedi';
import { Logger } from '@/Logger';
import { jsonParse, type IDataObject } from 'n8n-workflow';
import { jsonParse, type IDataObject, ApplicationError } from 'n8n-workflow';
import {
EXTERNAL_SECRETS_INITIAL_BACKOFF,
EXTERNAL_SECRETS_MAX_BACKOFF,
@ -90,7 +90,7 @@ export class ExternalSecretsManager {
try {
return jsonParse(decryptedData);
} catch (e) {
throw new Error(
throw new ApplicationError(
'External Secrets Settings could not be decrypted. The likely reason is that a different "encryptionKey" was used to encrypt the data.',
);
}

View file

@ -2,7 +2,7 @@ import type { SecretsProvider, SecretsProviderSettings, SecretsProviderState } f
import InfisicalClient from 'infisical-node';
import { populateClientWorkspaceConfigsHelper } from 'infisical-node/lib/helpers/key';
import { getServiceTokenData } from 'infisical-node/lib/api/serviceTokenData';
import type { IDataObject, INodeProperties } from 'n8n-workflow';
import { ApplicationError, type IDataObject, type INodeProperties } from 'n8n-workflow';
import { EXTERNAL_SECRETS_NAME_REGEX } from '../constants';
export interface InfisicalSettings {
@ -74,10 +74,10 @@ export class InfisicalProvider implements SecretsProvider {
async update(): Promise<void> {
if (!this.client) {
throw new Error('Updated attempted on Infisical when initialization failed');
throw new ApplicationError('Updated attempted on Infisical when initialization failed');
}
if (!(await this.test())[0]) {
throw new Error('Infisical provider test failed during update');
throw new ApplicationError('Infisical provider test failed during update');
}
const secrets = (await this.client.getAllSecrets({
environment: this.environment,
@ -120,7 +120,7 @@ export class InfisicalProvider implements SecretsProvider {
if (serviceTokenData.scopes) {
return serviceTokenData.scopes[0].environment;
}
throw new Error("Couldn't find environment for Infisical");
throw new ApplicationError("Couldn't find environment for Infisical");
}
async test(): Promise<[boolean] | [boolean, string]> {

View file

@ -1,3 +1,4 @@
import { ApplicationError } from 'n8n-workflow';
import { LdapService } from './LdapService.ee';
import { LdapSync } from './LdapSync.ee';
import type { LdapConfig } from './types';
@ -15,7 +16,7 @@ export class LdapManager {
sync: LdapSync;
} {
if (!this.initialized) {
throw new Error('LDAP Manager has not been initialized');
throw new ApplicationError('LDAP Manager has not been initialized');
}
return this.ldap;
}

View file

@ -4,6 +4,7 @@ import type { LdapConfig } from './types';
import { formatUrl, getMappingAttributes } from './helpers';
import { BINARY_AD_ATTRIBUTES } from './constants';
import type { ConnectionOptions } from 'tls';
import { ApplicationError } from 'n8n-workflow';
export class LdapService {
private client: Client | undefined;
@ -25,7 +26,7 @@ export class LdapService {
*/
private async getClient() {
if (this._config === undefined) {
throw new Error('Service cannot be used without setting the property config');
throw new ApplicationError('Service cannot be used without setting the property config');
}
if (this.client === undefined) {
const url = formatUrl(

View file

@ -17,6 +17,7 @@ import type { RunningMode, SyncStatus } from '@db/entities/AuthProviderSyncHisto
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
import { Logger } from '@/Logger';
import { ApplicationError } from 'n8n-workflow';
export class LdapSync {
private intervalId: NodeJS.Timeout | undefined = undefined;
@ -64,7 +65,7 @@ export class LdapSync {
*/
scheduleRun(): void {
if (!this._config.synchronizationInterval) {
throw new Error('Interval variable has to be defined');
throw new ApplicationError('Interval variable has to be defined');
}
this.intervalId = setInterval(async () => {
await this.run('live');

View file

@ -20,7 +20,7 @@ import {
LDAP_LOGIN_LABEL,
} from './constants';
import type { ConnectionSecurity, LdapConfig } from './types';
import { jsonParse } from 'n8n-workflow';
import { ApplicationError, jsonParse } from 'n8n-workflow';
import { License } from '@/License';
import { InternalHooks } from '@/InternalHooks';
import {
@ -157,7 +157,7 @@ export const updateLdapConfig = async (ldapConfig: LdapConfig): Promise<void> =>
const { valid, message } = validateLdapConfigurationSchema(ldapConfig);
if (!valid) {
throw new Error(message);
throw new ApplicationError(message);
}
if (ldapConfig.loginEnabled && getCurrentAuthenticationMethod() === 'saml') {

View file

@ -17,7 +17,7 @@ import type {
INodeTypeData,
ICredentialTypeData,
} from 'n8n-workflow';
import { ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
import { ApplicationError, ErrorReporterProxy as ErrorReporter } from 'n8n-workflow';
import config from '@/config';
import {
@ -56,7 +56,7 @@ export class LoadNodesAndCredentials {
) {}
async init() {
if (inTest) throw new Error('Not available in tests');
if (inTest) throw new ApplicationError('Not available in tests');
// Make sure the imported modules can resolve dependencies fine.
const delimiter = process.platform === 'win32' ? ';' : ':';

View file

@ -6,7 +6,7 @@ import type {
IVersionedNodeType,
LoadedClass,
} from 'n8n-workflow';
import { NodeHelpers } from 'n8n-workflow';
import { ApplicationError, NodeHelpers } from 'n8n-workflow';
import { Service } from 'typedi';
import { LoadNodesAndCredentials } from './LoadNodesAndCredentials';
import { join, dirname } from 'path';
@ -30,7 +30,7 @@ export class NodeTypes implements INodeTypes {
const nodeType = this.getNode(nodeTypeName);
if (!nodeType) {
throw new Error(`Unknown node type: ${nodeTypeName}`);
throw new ApplicationError('Unknown node type', { tags: { nodeTypeName } });
}
const { description } = NodeHelpers.getVersionedNodeType(nodeType.type, version);

View file

@ -1,6 +1,10 @@
import type Bull from 'bull';
import { Service } from 'typedi';
import type { ExecutionError, IExecuteResponsePromiseData } from 'n8n-workflow';
import {
ApplicationError,
type ExecutionError,
type IExecuteResponsePromiseData,
} from 'n8n-workflow';
import { ActiveExecutions } from '@/ActiveExecutions';
import { decodeWebhookResponse } from '@/helpers/decodeWebhookResponse';
@ -96,7 +100,7 @@ export class Queue {
getBullObjectInstance(): JobQueue {
if (this.jobQueue === undefined) {
// if queue is not initialized yet throw an error, since we do not want to hand around an undefined queue
throw new Error('Queue is not initialized yet!');
throw new ApplicationError('Queue is not initialized yet!');
}
return this.jobQueue;
}

View file

@ -27,7 +27,7 @@ import type {
IExecutionsSummary,
IN8nUISettings,
} from 'n8n-workflow';
import { jsonParse } from 'n8n-workflow';
import { ApplicationError, jsonParse } from 'n8n-workflow';
// @ts-ignore
import timezones from 'google-timezones-json';
@ -672,7 +672,9 @@ export class Server extends AbstractServer {
const job = currentJobs.find((job) => job.data.executionId === req.params.id);
if (!job) {
throw new Error(`Could not stop "${req.params.id}" as it is no longer in queue.`);
throw new ApplicationError('Could not stop job because it is no longer in queue.', {
extra: { jobId: req.params.id },
});
} else {
await queue.stopJob(job);
}

View file

@ -1,13 +1,14 @@
import type express from 'express';
import { Service } from 'typedi';
import type {
IWebhookData,
IWorkflowExecuteAdditionalData,
IHttpRequestMethods,
Workflow,
WorkflowActivateMode,
WorkflowExecuteMode,
import {
type IWebhookData,
type IWorkflowExecuteAdditionalData,
type IHttpRequestMethods,
type Workflow,
type WorkflowActivateMode,
type WorkflowExecuteMode,
ApplicationError,
} from 'n8n-workflow';
import { ActiveWebhooks } from '@/ActiveWebhooks';
@ -215,7 +216,9 @@ export class TestWebhooks implements IWebhookManager {
}
if (workflow.id === undefined) {
throw new Error('Webhooks can only be added for saved workflows as an id is needed!');
throw new ApplicationError(
'Webhooks can only be added for saved workflows as an ID is needed',
);
}
// Remove test-webhooks automatically if they do not get called (after 120 seconds)

View file

@ -11,6 +11,7 @@ import { getWebhookBaseUrl } from '@/WebhookHelpers';
import { RoleService } from '@/services/role.service';
import { UserRepository } from '@db/repositories/user.repository';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ApplicationError } from 'n8n-workflow';
export function isSharingEnabled(): boolean {
return Container.get(License).isSharingEnabled();
@ -94,14 +95,15 @@ export const hashPassword = async (validPassword: string): Promise<string> =>
export async function compareHash(plaintext: string, hashed: string): Promise<boolean | undefined> {
try {
return await compare(plaintext, hashed);
} catch (error) {
} catch (e) {
const error = e instanceof Error ? e : new Error(`${e}`);
if (error instanceof Error && error.message.includes('Invalid salt version')) {
error.message +=
'. Comparison against unhashed string. Please check that the value compared against has been hashed.';
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
throw new Error(error);
throw new ApplicationError(error.message, { cause: error });
}
}

View file

@ -6,6 +6,7 @@ import { Container, Service } from 'typedi';
import config from '@/config';
import type { InviteEmailData, PasswordResetData, SendEmailResult } from './Interfaces';
import { NodeMailer } from './NodeMailer';
import { ApplicationError } from 'n8n-workflow';
type Template = HandlebarsTemplateDelegate<unknown>;
type TemplateName = 'invite' | 'passwordReset';
@ -50,7 +51,7 @@ export class UserManagementMailer {
}
async verifyConnection(): Promise<void> {
if (!this.mailer) throw new Error('No mailer configured.');
if (!this.mailer) throw new ApplicationError('No mailer configured.');
return this.mailer.verifyConnection();
}

View file

@ -1,4 +1,8 @@
import { ErrorReporterProxy as ErrorReporter, WorkflowOperationError } from 'n8n-workflow';
import {
ApplicationError,
ErrorReporterProxy as ErrorReporter,
WorkflowOperationError,
} from 'n8n-workflow';
import { Container, Service } from 'typedi';
import type { FindManyOptions, ObjectLiteral } from 'typeorm';
import { Not, LessThanOrEqual } from 'typeorm';
@ -106,7 +110,9 @@ export class WaitTracker {
});
if (!execution) {
throw new Error(`The execution ID "${executionId}" could not be found.`);
throw new ApplicationError('Execution not found.', {
extra: { executionId },
});
}
if (!['new', 'unknown', 'waiting', 'running'].includes(execution.status)) {
@ -129,7 +135,9 @@ export class WaitTracker {
},
);
if (!restoredExecution) {
throw new Error(`Execution ${executionId} could not be recovered or canceled.`);
throw new ApplicationError('Execution could not be recovered or canceled.', {
extra: { executionId },
});
}
fullExecutionData = restoredExecution;
}
@ -172,14 +180,14 @@ export class WaitTracker {
});
if (!fullExecutionData) {
throw new Error(`The execution with the id "${executionId}" does not exist.`);
throw new ApplicationError('Execution does not exist.', { extra: { executionId } });
}
if (fullExecutionData.finished) {
throw new Error('The execution did succeed and can so not be started again.');
throw new ApplicationError('The execution did succeed and can so not be started again.');
}
if (!fullExecutionData.workflowData.id) {
throw new Error('Only saved workflows can be resumed.');
throw new ApplicationError('Only saved workflows can be resumed.');
}
const workflowId = fullExecutionData.workflowData.id;
const user = await this.ownershipService.getWorkflowOwnerCached(workflowId);

View file

@ -1,5 +1,5 @@
import Container from 'typedi';
import type { INode, IWorkflowCredentials } from 'n8n-workflow';
import { ApplicationError, type INode, type IWorkflowCredentials } from 'n8n-workflow';
import { CredentialsRepository } from '@db/repositories/credentials.repository';
// eslint-disable-next-line @typescript-eslint/naming-convention
@ -24,8 +24,9 @@ export async function WorkflowCredentials(nodes: INode[]): Promise<IWorkflowCred
nodeCredentials = node.credentials[type];
if (!nodeCredentials.id) {
throw new Error(
throw new ApplicationError(
`Credentials with name "${nodeCredentials.name}" for type "${type}" miss an ID.`,
{ extra: { credentialName: nodeCredentials.name }, tags: { credentialType: type } },
);
}
@ -35,9 +36,10 @@ export async function WorkflowCredentials(nodes: INode[]): Promise<IWorkflowCred
type,
});
if (!foundCredentials) {
throw new Error(
`Could not find credentials for type "${type}" with ID "${nodeCredentials.id}".`,
);
throw new ApplicationError('Could not find credential.', {
tags: { credentialType: type },
extra: { credentialId: nodeCredentials.id },
});
}
returnCredentials[type][nodeCredentials.id] = foundCredentials;

View file

@ -26,6 +26,7 @@ import type {
ExecutionError,
} from 'n8n-workflow';
import {
ApplicationError,
ErrorReporterProxy as ErrorReporter,
NodeOperationError,
Workflow,
@ -679,7 +680,7 @@ export async function getWorkflowData(
parentWorkflowSettings?: IWorkflowSettings,
): Promise<IWorkflowBase> {
if (workflowInfo.id === undefined && workflowInfo.code === undefined) {
throw new Error(
throw new ApplicationError(
'No information about the workflow to execute found. Please provide either the "id" or "code"!',
);
}
@ -691,7 +692,9 @@ export async function getWorkflowData(
workflowData = await WorkflowsService.get({ id: workflowInfo.id }, { relations });
if (workflowData === undefined || workflowData === null) {
throw new Error(`The workflow with the id "${workflowInfo.id}" does not exist.`);
throw new ApplicationError('Workflow does not exist.', {
extra: { workflowId: workflowInfo.id },
});
}
} else {
workflowData = workflowInfo.code ?? null;

View file

@ -10,6 +10,7 @@ import { UserRepository } from '@db/repositories/user.repository';
import { JwtService } from '@/services/jwt.service';
import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
import { AuthError } from '@/errors/response-errors/auth.error';
import { ApplicationError } from 'n8n-workflow';
export function issueJWT(user: User): JwtToken {
const { id, email, password } = user;
@ -70,7 +71,7 @@ export async function resolveJwtContent(jwtPayload: JwtPayload): Promise<User> {
if (!user || jwtPayload.password !== passwordHash || user.email !== jwtPayload.email) {
// When owner hasn't been set up, the default user
// won't have email nor password (both equals null)
throw new Error('Invalid token content');
throw new ApplicationError('Invalid token content');
}
return user;
}

View file

@ -2,7 +2,7 @@ import 'reflect-metadata';
import { Command } from '@oclif/command';
import { ExitError } from '@oclif/errors';
import { Container } from 'typedi';
import { ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow';
import { ApplicationError, ErrorReporterProxy as ErrorReporter, sleep } from 'n8n-workflow';
import { BinaryDataService, InstanceSettings, ObjectStoreService } from 'n8n-core';
import type { AbstractServer } from '@/AbstractServer';
import { Logger } from '@/Logger';
@ -127,7 +127,7 @@ export abstract class BaseCommand extends Command {
if (!isSelected && !isAvailable) return;
if (isSelected && !isAvailable) {
throw new Error(
throw new ApplicationError(
'External storage selected but unavailable. Please make external storage available by adding "s3" to `N8N_AVAILABLE_BINARY_DATA_MODES`.',
);
}
@ -171,7 +171,7 @@ export abstract class BaseCommand extends Command {
const host = config.getEnv('externalStorage.s3.host');
if (host === '') {
throw new Error(
throw new ApplicationError(
'External storage host not configured. Please set `N8N_EXTERNAL_STORAGE_S3_HOST`.',
);
}
@ -182,13 +182,13 @@ export abstract class BaseCommand extends Command {
};
if (bucket.name === '') {
throw new Error(
throw new ApplicationError(
'External storage bucket name not configured. Please set `N8N_EXTERNAL_STORAGE_S3_BUCKET_NAME`.',
);
}
if (bucket.region === '') {
throw new Error(
throw new ApplicationError(
'External storage bucket region not configured. Please set `N8N_EXTERNAL_STORAGE_S3_BUCKET_REGION`.',
);
}
@ -199,13 +199,13 @@ export abstract class BaseCommand extends Command {
};
if (credentials.accessKey === '') {
throw new Error(
throw new ApplicationError(
'External storage access key not configured. Please set `N8N_EXTERNAL_STORAGE_S3_ACCESS_KEY`.',
);
}
if (credentials.accessSecret === '') {
throw new Error(
throw new ApplicationError(
'External storage access secret not configured. Please set `N8N_EXTERNAL_STORAGE_S3_ACCESS_SECRET`.',
);
}

View file

@ -6,6 +6,7 @@ import type { Risk } from '@/security-audit/types';
import { BaseCommand } from './BaseCommand';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
import { ApplicationError } from 'n8n-workflow';
export class SecurityAudit extends BaseCommand {
static description = 'Generate a security audit report for this n8n instance';
@ -46,7 +47,7 @@ export class SecurityAudit extends BaseCommand {
const hint = `Valid categories are: ${RISK_CATEGORIES.join(', ')}`;
throw new Error([message, hint].join('. '));
throw new ApplicationError([message, hint].join('. '));
}
const result = await Container.get(SecurityAuditService).run(

View file

@ -2,7 +2,7 @@ import { promises as fs } from 'fs';
import { flags } from '@oclif/command';
import { PLACEHOLDER_EMPTY_WORKFLOW_ID } from 'n8n-core';
import type { IWorkflowBase } from 'n8n-workflow';
import { ExecutionBaseError } from 'n8n-workflow';
import { ApplicationError, ExecutionBaseError } from 'n8n-workflow';
import { ActiveExecutions } from '@/ActiveExecutions';
import { WorkflowRunner } from '@/WorkflowRunner';
@ -89,7 +89,7 @@ export class Execute extends BaseCommand {
}
if (!workflowData) {
throw new Error('Failed to retrieve workflow data for requested workflow');
throw new ApplicationError('Failed to retrieve workflow data for requested workflow');
}
if (!isWorkflowIdValid(workflowId)) {
@ -113,7 +113,7 @@ export class Execute extends BaseCommand {
const data = await activeExecutions.getPostExecutePromise(executionId);
if (data === undefined) {
throw new Error('Workflow did not return any data!');
throw new ApplicationError('Workflow did not return any data');
}
if (data.data.resultData.error) {

View file

@ -3,7 +3,7 @@ import fs from 'fs';
import os from 'os';
import { flags } from '@oclif/command';
import type { IRun, ITaskData } from 'n8n-workflow';
import { jsonParse, sleep } from 'n8n-workflow';
import { ApplicationError, jsonParse, sleep } from 'n8n-workflow';
import { sep } from 'path';
import { diff } from 'json-diff';
import pick from 'lodash/pick';
@ -486,7 +486,7 @@ export class ExecuteBatch extends BaseCommand {
this.updateStatus();
}
} else {
throw new Error('Wrong execution status - cannot proceed');
throw new ApplicationError('Wrong execution status - cannot proceed');
}
});
}

View file

@ -7,6 +7,7 @@ import type { ICredentialsDb, ICredentialsDecryptedDb } from '@/Interfaces';
import { BaseCommand } from '../BaseCommand';
import { CredentialsRepository } from '@db/repositories/credentials.repository';
import Container from 'typedi';
import { ApplicationError } from 'n8n-workflow';
export class ExportCredentialsCommand extends BaseCommand {
static description = 'Export credentials';
@ -125,7 +126,7 @@ export class ExportCredentialsCommand extends BaseCommand {
}
if (credentials.length === 0) {
throw new Error('No credentials found with specified filters.');
throw new ApplicationError('No credentials found with specified filters');
}
if (flags.separate) {

View file

@ -6,6 +6,7 @@ import type { WorkflowEntity } from '@db/entities/WorkflowEntity';
import { BaseCommand } from '../BaseCommand';
import { WorkflowRepository } from '@db/repositories/workflow.repository';
import Container from 'typedi';
import { ApplicationError } from 'n8n-workflow';
export class ExportWorkflowsCommand extends BaseCommand {
static description = 'Export workflows';
@ -111,7 +112,7 @@ export class ExportWorkflowsCommand extends BaseCommand {
});
if (workflows.length === 0) {
throw new Error('No workflows found with specified filters.');
throw new ApplicationError('No workflows found with specified filters');
}
if (flags.separate) {

View file

@ -12,7 +12,7 @@ import { CredentialsEntity } from '@db/entities/CredentialsEntity';
import { disableAutoGeneratedIds } from '@db/utils/commandHelpers';
import { BaseCommand } from '../BaseCommand';
import type { ICredentialsEncrypted } from 'n8n-workflow';
import { jsonParse } from 'n8n-workflow';
import { ApplicationError, jsonParse } from 'n8n-workflow';
import { RoleService } from '@/services/role.service';
import { UM_FIX_INSTRUCTION } from '@/constants';
import { UserRepository } from '@db/repositories/user.repository';
@ -113,7 +113,7 @@ export class ImportCredentialsCommand extends BaseCommand {
totalImported = credentials.length;
if (!Array.isArray(credentials)) {
throw new Error(
throw new ApplicationError(
'File does not seem to contain credentials. Make sure the credentials are contained in an array.',
);
}
@ -149,7 +149,7 @@ export class ImportCredentialsCommand extends BaseCommand {
const ownerCredentialRole = await Container.get(RoleService).findCredentialOwnerRole();
if (!ownerCredentialRole) {
throw new Error(`Failed to find owner credential role. ${UM_FIX_INSTRUCTION}`);
throw new ApplicationError(`Failed to find owner credential role. ${UM_FIX_INSTRUCTION}`);
}
this.ownerCredentialRole = ownerCredentialRole;
@ -179,7 +179,7 @@ export class ImportCredentialsCommand extends BaseCommand {
(await Container.get(UserRepository).findOneBy({ globalRoleId: ownerGlobalRole.id }));
if (!owner) {
throw new Error(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
}
return owner;
@ -189,7 +189,7 @@ export class ImportCredentialsCommand extends BaseCommand {
const user = await Container.get(UserRepository).findOneBy({ id: userId });
if (!user) {
throw new Error(`Failed to find user with ID ${userId}`);
throw new ApplicationError('Failed to find user', { extra: { userId } });
}
return user;

View file

@ -1,6 +1,6 @@
import { flags } from '@oclif/command';
import type { INode, INodeCredentialsDetails } from 'n8n-workflow';
import { jsonParse } from 'n8n-workflow';
import { ApplicationError, jsonParse } from 'n8n-workflow';
import fs from 'fs';
import glob from 'fast-glob';
import { Container } from 'typedi';
@ -24,7 +24,7 @@ import { CredentialsRepository } from '@db/repositories/credentials.repository';
function assertHasWorkflowsToImport(workflows: unknown): asserts workflows is IWorkflowToImport[] {
if (!Array.isArray(workflows)) {
throw new Error(
throw new ApplicationError(
'File does not seem to contain workflows. Make sure the workflows are contained in an array.',
);
}
@ -35,7 +35,7 @@ function assertHasWorkflowsToImport(workflows: unknown): asserts workflows is IW
!Object.prototype.hasOwnProperty.call(workflow, 'nodes') ||
!Object.prototype.hasOwnProperty.call(workflow, 'connections')
) {
throw new Error('File does not seem to contain valid workflows.');
throw new ApplicationError('File does not seem to contain valid workflows.');
}
}
}
@ -217,7 +217,7 @@ export class ImportWorkflowsCommand extends BaseCommand {
const ownerWorkflowRole = await Container.get(RoleService).findWorkflowOwnerRole();
if (!ownerWorkflowRole) {
throw new Error(`Failed to find owner workflow role. ${UM_FIX_INSTRUCTION}`);
throw new ApplicationError(`Failed to find owner workflow role. ${UM_FIX_INSTRUCTION}`);
}
this.ownerWorkflowRole = ownerWorkflowRole;
@ -244,7 +244,7 @@ export class ImportWorkflowsCommand extends BaseCommand {
(await Container.get(UserRepository).findOneBy({ globalRoleId: ownerGlobalRole?.id }));
if (!owner) {
throw new Error(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
}
return owner;
@ -254,7 +254,7 @@ export class ImportWorkflowsCommand extends BaseCommand {
const user = await Container.get(UserRepository).findOneBy({ id: userId });
if (!user) {
throw new Error(`Failed to find user with ID ${userId}`);
throw new ApplicationError('Failed to find user', { extra: { userId } });
}
return user;

View file

@ -13,7 +13,7 @@ import type {
INodeTypes,
IRun,
} from 'n8n-workflow';
import { Workflow, NodeOperationError, sleep } from 'n8n-workflow';
import { Workflow, NodeOperationError, sleep, ApplicationError } from 'n8n-workflow';
import * as Db from '@/Db';
import * as ResponseHelper from '@/ResponseHelper';
@ -125,8 +125,9 @@ export class Worker extends BaseCommand {
`Worker failed to find data of execution "${executionId}" in database. Cannot continue.`,
{ executionId },
);
throw new Error(
`Unable to find data of execution "${executionId}" in database. Aborting execution.`,
throw new ApplicationError(
'Unable to find data of execution in database. Aborting execution.',
{ extra: { executionId } },
);
}
const workflowId = fullExecutionData.workflowData.id!;
@ -150,7 +151,7 @@ export class Worker extends BaseCommand {
'Worker execution failed because workflow could not be found in database.',
{ workflowId, executionId },
);
throw new Error(`The workflow with the ID "${workflowId}" could not be found`);
throw new ApplicationError('Workflow could not be found', { extra: { workflowId } });
}
staticData = workflowData.staticData;
}
@ -408,7 +409,7 @@ export class Worker extends BaseCommand {
try {
if (!connection.isInitialized) {
// Connection is not active
throw new Error('No active database connection!');
throw new ApplicationError('No active database connection');
}
// DB ping
await connection.query('SELECT 1');

View file

@ -1,7 +1,7 @@
import convict from 'convict';
import dotenv from 'dotenv';
import { readFileSync } from 'fs';
import { setGlobalState } from 'n8n-workflow';
import { ApplicationError, setGlobalState } from 'n8n-workflow';
import { inTest, inE2ETests } from '@/constants';
if (inE2ETests) {
@ -53,7 +53,7 @@ if (!inE2ETests && !inTest) {
} catch (error) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (error.code === 'ENOENT') {
throw new Error(`The file "${fileName}" could not be found.`);
throw new ApplicationError('File not found', { extra: { fileName } });
}
throw error;
}

View file

@ -1,8 +1,9 @@
import { NotStringArrayError } from '@/errors/not-string-array.error';
import type { SchemaObj } from 'convict';
import { ApplicationError } from 'n8n-workflow';
export const ensureStringArray = (values: string[], { env }: SchemaObj<string>) => {
if (!env) throw new Error(`Missing env: ${env}`);
if (!env) throw new ApplicationError('Missing env', { extra: { env } });
if (!Array.isArray(values)) throw new NotStringArrayError(env);

View file

@ -25,6 +25,7 @@ import { AuthError } from '@/errors/response-errors/auth.error';
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
import { ApplicationError } from 'n8n-workflow';
@Service()
@RestController()
@ -44,8 +45,8 @@ export class AuthController {
@Post('/login')
async login(req: LoginRequest, res: Response): Promise<PublicUser | undefined> {
const { email, password, mfaToken, mfaRecoveryCode } = req.body;
if (!email) throw new Error('Email is required to log in');
if (!password) throw new Error('Password is required to log in');
if (!email) throw new ApplicationError('Email is required to log in');
if (!password) throw new ApplicationError('Password is required to log in');
let user: User | undefined;

View file

@ -9,7 +9,7 @@ import omit from 'lodash/omit';
import set from 'lodash/set';
import split from 'lodash/split';
import type { OAuth2GrantType } from 'n8n-workflow';
import { jsonParse, jsonStringify } from 'n8n-workflow';
import { ApplicationError, jsonParse, jsonStringify } from 'n8n-workflow';
import { Authorized, Get, RestController } from '@/decorators';
import { OAuthRequest } from '@/requests';
import { AbstractOAuthController } from './abstractOAuth.controller';
@ -255,7 +255,7 @@ export class OAuth2CredentialController extends AbstractOAuthController {
errorMessage,
});
if (typeof decoded.cid !== 'string' || typeof decoded.token !== 'string') {
throw new Error(errorMessage);
throw new ApplicationError(errorMessage);
}
return decoded;
}

View file

@ -2,6 +2,7 @@ import type { TableForeignKeyOptions, TableIndexOptions, QueryRunner } from 'typ
import { Table, TableColumn } from 'typeorm';
import LazyPromise from 'p-lazy';
import { Column } from './Column';
import { ApplicationError } from 'n8n-workflow';
abstract class TableOperation<R = void> extends LazyPromise<R> {
abstract execute(queryRunner: QueryRunner): Promise<R>;
@ -131,7 +132,7 @@ class ModifyNotNull extends TableOperation {
async execute(queryRunner: QueryRunner) {
const { tableName, prefix, columnName, isNullable } = this;
const table = await queryRunner.getTable(`${prefix}${tableName}`);
if (!table) throw new Error(`No table found with the name ${tableName}`);
if (!table) throw new ApplicationError('No table found', { extra: { tableName } });
const oldColumn = table.findColumnByName(columnName)!;
const newColumn = oldColumn.clone();
newColumn.isNullable = isNullable;

View file

@ -1,4 +1,5 @@
import type { MigrationContext, ReversibleMigration } from '@db/types';
import { ApplicationError } from 'n8n-workflow';
export class AddGlobalAdminRole1700571993961 implements ReversibleMigration {
async up({ escape, runQuery }: MigrationContext) {
@ -39,7 +40,7 @@ export class AddGlobalAdminRole1700571993961 implements ReversibleMigration {
const memberRoleId = memberRoleIdResult[0]?.id;
if (!memberRoleId) {
throw new Error('Could not find global member role!');
throw new ApplicationError('Could not find global member role!');
}
await runQuery(

View file

@ -8,7 +8,12 @@ import type {
SelectQueryBuilder,
} from 'typeorm';
import { parse, stringify } from 'flatted';
import type { ExecutionStatus, IExecutionsSummary, IRunExecutionData } from 'n8n-workflow';
import {
ApplicationError,
type ExecutionStatus,
type IExecutionsSummary,
type IRunExecutionData,
} from 'n8n-workflow';
import { BinaryDataService } from 'n8n-core';
import type {
ExecutionPayload,
@ -381,7 +386,9 @@ export class ExecutionRepository extends Repository<ExecutionEntity> {
},
) {
if (!deleteConditions?.deleteBefore && !deleteConditions?.ids) {
throw new Error('Either "deleteBefore" or "ids" must be present in the request body');
throw new ApplicationError(
'Either "deleteBefore" or "ids" must be present in the request body',
);
}
const query = this.createQueryBuilder('execution')

View file

@ -3,7 +3,7 @@ import { readFileSync, rmSync } from 'fs';
import { InstanceSettings } from 'n8n-core';
import type { ObjectLiteral } from 'typeorm';
import type { QueryRunner } from 'typeorm/query-runner/QueryRunner';
import { jsonParse } from 'n8n-workflow';
import { ApplicationError, jsonParse } from 'n8n-workflow';
import config from '@/config';
import { inTest } from '@/constants';
import type { BaseMigration, Migration, MigrationContext, MigrationFn } from '@db/types';
@ -23,7 +23,7 @@ function loadSurveyFromDisk(): string | null {
const personalizationSurvey = JSON.parse(surveyFile) as object;
const kvPairs = Object.entries(personalizationSurvey);
if (!kvPairs.length) {
throw new Error('personalizationSurvey is empty');
throw new ApplicationError('personalizationSurvey is empty');
} else {
const emptyKeys = kvPairs.reduce((acc, [, value]) => {
if (!value || (Array.isArray(value) && !value.length)) {
@ -32,7 +32,7 @@ function loadSurveyFromDisk(): string | null {
return acc;
}, 0);
if (emptyKeys === kvPairs.length) {
throw new Error('incomplete personalizationSurvey');
throw new ApplicationError('incomplete personalizationSurvey');
}
}
return surveyFile;
@ -68,7 +68,8 @@ const runDisablingForeignKeys = async (
fn: MigrationFn,
) => {
const { dbType, queryRunner } = context;
if (dbType !== 'sqlite') throw new Error('Disabling transactions only available in sqlite');
if (dbType !== 'sqlite')
throw new ApplicationError('Disabling transactions only available in sqlite');
await queryRunner.query('PRAGMA foreign_keys=OFF');
await queryRunner.startTransaction();
try {

View file

@ -24,6 +24,7 @@ import type { BooleanLicenseFeature } from '@/Interfaces';
import Container from 'typedi';
import { License } from '@/License';
import type { Scope } from '@n8n/permissions';
import { ApplicationError } from 'n8n-workflow';
export const createAuthMiddleware =
(authRole: AuthRole): RequestHandler =>
@ -87,7 +88,9 @@ export const registerController = (app: Application, config: Config, cObj: objec
| string
| undefined;
if (!controllerBasePath)
throw new Error(`${controllerClass.name} is missing the RestController decorator`);
throw new ApplicationError('Controller is missing the RestController decorator', {
extra: { controllerName: controllerClass.name },
});
const authRoles = Reflect.getMetadata(CONTROLLER_AUTH_ROLES, controllerClass) as
| AuthRoleMetadata

View file

@ -35,6 +35,7 @@ import { InternalHooks } from '@/InternalHooks';
import { TagRepository } from '@db/repositories/tag.repository';
import { Logger } from '@/Logger';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ApplicationError } from 'n8n-workflow';
@Service()
export class SourceControlService {
@ -83,7 +84,7 @@ export class SourceControlService {
false,
);
if (!foldersExisted) {
throw new Error();
throw new ApplicationError('No folders exist');
}
if (!this.gitService.git) {
await this.initGitService();
@ -94,7 +95,7 @@ export class SourceControlService {
branches.current !==
this.sourceControlPreferencesService.sourceControlPreferences.branchName
) {
throw new Error();
throw new ApplicationError('Branch is not set up correctly');
}
} catch (error) {
throw new BadRequestError(
@ -195,7 +196,7 @@ export class SourceControlService {
await this.gitService.pull();
} catch (error) {
this.logger.error(`Failed to reset workfolder: ${(error as Error).message}`);
throw new Error(
throw new ApplicationError(
'Unable to fetch updates from git - your folder might be out of sync. Try reconnecting from the Source Control settings page.',
);
}

View file

@ -22,6 +22,7 @@ import { sourceControlFoldersExistCheck } from './sourceControlHelper.ee';
import type { User } from '@db/entities/User';
import { getInstanceOwner } from '../../UserManagement/UserManagementHelper';
import { Logger } from '@/Logger';
import { ApplicationError } from 'n8n-workflow';
@Service()
export class SourceControlGitService {
@ -43,7 +44,7 @@ export class SourceControlGitService {
});
this.logger.debug(`Git binary found: ${gitResult.toString()}`);
} catch (error) {
throw new Error(`Git binary not found: ${(error as Error).message}`);
throw new ApplicationError('Git binary not found', { cause: error });
}
try {
const sshResult = execSync('ssh -V', {
@ -51,7 +52,7 @@ export class SourceControlGitService {
});
this.logger.debug(`SSH binary found: ${sshResult.toString()}`);
} catch (error) {
throw new Error(`SSH binary not found: ${(error as Error).message}`);
throw new ApplicationError('SSH binary not found', { cause: error });
}
return true;
}
@ -114,7 +115,7 @@ export class SourceControlGitService {
private async checkRepositorySetup(): Promise<boolean> {
if (!this.git) {
throw new Error('Git is not initialized (async)');
throw new ApplicationError('Git is not initialized (async)');
}
if (!(await this.git.checkIsRepo())) {
return false;
@ -129,7 +130,7 @@ export class SourceControlGitService {
private async hasRemote(remote: string): Promise<boolean> {
if (!this.git) {
throw new Error('Git is not initialized (async)');
throw new ApplicationError('Git is not initialized (async)');
}
try {
const remotes = await this.git.getRemotes(true);
@ -141,7 +142,7 @@ export class SourceControlGitService {
return true;
}
} catch (error) {
throw new Error(`Git is not initialized ${(error as Error).message}`);
throw new ApplicationError('Git is not initialized', { cause: error });
}
this.logger.debug(`Git remote not found: ${remote}`);
return false;
@ -155,7 +156,7 @@ export class SourceControlGitService {
user: User,
): Promise<void> {
if (!this.git) {
throw new Error('Git is not initialized (Promise)');
throw new ApplicationError('Git is not initialized (Promise)');
}
if (sourceControlPreferences.initRepo) {
try {
@ -193,7 +194,7 @@ export class SourceControlGitService {
async setGitUserDetails(name: string, email: string): Promise<void> {
if (!this.git) {
throw new Error('Git is not initialized (setGitUserDetails)');
throw new ApplicationError('Git is not initialized (setGitUserDetails)');
}
await this.git.addConfig('user.email', email);
await this.git.addConfig('user.name', name);
@ -201,7 +202,7 @@ export class SourceControlGitService {
async getBranches(): Promise<{ branches: string[]; currentBranch: string }> {
if (!this.git) {
throw new Error('Git is not initialized (getBranches)');
throw new ApplicationError('Git is not initialized (getBranches)');
}
try {
@ -218,13 +219,13 @@ export class SourceControlGitService {
currentBranch: current,
};
} catch (error) {
throw new Error(`Could not get remote branches from repository ${(error as Error).message}`);
throw new ApplicationError('Could not get remote branches from repository', { cause: error });
}
}
async setBranch(branch: string): Promise<{ branches: string[]; currentBranch: string }> {
if (!this.git) {
throw new Error('Git is not initialized (setBranch)');
throw new ApplicationError('Git is not initialized (setBranch)');
}
await this.git.checkout(branch);
await this.git.branch([`--set-upstream-to=${SOURCE_CONTROL_ORIGIN}/${branch}`, branch]);
@ -233,7 +234,7 @@ export class SourceControlGitService {
async getCurrentBranch(): Promise<{ current: string; remote: string }> {
if (!this.git) {
throw new Error('Git is not initialized (getCurrentBranch)');
throw new ApplicationError('Git is not initialized (getCurrentBranch)');
}
const currentBranch = (await this.git.branch()).current;
return {
@ -244,7 +245,7 @@ export class SourceControlGitService {
async diffRemote(): Promise<DiffResult | undefined> {
if (!this.git) {
throw new Error('Git is not initialized (diffRemote)');
throw new ApplicationError('Git is not initialized (diffRemote)');
}
const currentBranch = await this.getCurrentBranch();
if (currentBranch.remote) {
@ -256,7 +257,7 @@ export class SourceControlGitService {
async diffLocal(): Promise<DiffResult | undefined> {
if (!this.git) {
throw new Error('Git is not initialized (diffLocal)');
throw new ApplicationError('Git is not initialized (diffLocal)');
}
const currentBranch = await this.getCurrentBranch();
if (currentBranch.remote) {
@ -268,14 +269,14 @@ export class SourceControlGitService {
async fetch(): Promise<FetchResult> {
if (!this.git) {
throw new Error('Git is not initialized (fetch)');
throw new ApplicationError('Git is not initialized (fetch)');
}
return this.git.fetch();
}
async pull(options: { ffOnly: boolean } = { ffOnly: true }): Promise<PullResult> {
if (!this.git) {
throw new Error('Git is not initialized (pull)');
throw new ApplicationError('Git is not initialized (pull)');
}
const params = {};
if (options.ffOnly) {
@ -293,7 +294,7 @@ export class SourceControlGitService {
): Promise<PushResult> {
const { force, branch } = options;
if (!this.git) {
throw new Error('Git is not initialized ({)');
throw new ApplicationError('Git is not initialized ({)');
}
if (force) {
return this.git.push(SOURCE_CONTROL_ORIGIN, branch, ['-f']);
@ -303,7 +304,7 @@ export class SourceControlGitService {
async stage(files: Set<string>, deletedFiles?: Set<string>): Promise<string> {
if (!this.git) {
throw new Error('Git is not initialized (stage)');
throw new ApplicationError('Git is not initialized (stage)');
}
if (deletedFiles?.size) {
try {
@ -319,7 +320,7 @@ export class SourceControlGitService {
options: { hard: boolean; target: string } = { hard: true, target: 'HEAD' },
): Promise<string> {
if (!this.git) {
throw new Error('Git is not initialized (Promise)');
throw new ApplicationError('Git is not initialized (Promise)');
}
if (options?.hard) {
return this.git.raw(['reset', '--hard', options.target]);
@ -331,14 +332,14 @@ export class SourceControlGitService {
async commit(message: string): Promise<CommitResult> {
if (!this.git) {
throw new Error('Git is not initialized (commit)');
throw new ApplicationError('Git is not initialized (commit)');
}
return this.git.commit(message);
}
async status(): Promise<StatusResult> {
if (!this.git) {
throw new Error('Git is not initialized (status)');
throw new ApplicationError('Git is not initialized (status)');
}
const statusResult = await this.git.status();
return statusResult;

View file

@ -8,7 +8,7 @@ import {
SOURCE_CONTROL_WORKFLOW_EXPORT_FOLDER,
} from './constants';
import glob from 'fast-glob';
import { jsonParse } from 'n8n-workflow';
import { ApplicationError, jsonParse } from 'n8n-workflow';
import { readFile as fsReadFile } from 'fs/promises';
import { Credentials, InstanceSettings } from 'n8n-core';
import type { IWorkflowToImport } from '@/Interfaces';
@ -63,7 +63,7 @@ export class SourceControlImportService {
const globalOwnerRole = await Container.get(RoleService).findGlobalOwnerRole();
if (!globalOwnerRole) {
throw new Error(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
}
return globalOwnerRole;
@ -73,7 +73,7 @@ export class SourceControlImportService {
const credentialOwnerRole = await Container.get(RoleService).findCredentialOwnerRole();
if (!credentialOwnerRole) {
throw new Error(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
throw new ApplicationError(`Failed to find owner. ${UM_FIX_INSTRUCTION}`);
}
return credentialOwnerRole;
@ -83,7 +83,7 @@ export class SourceControlImportService {
const workflowOwnerRole = await Container.get(RoleService).findWorkflowOwnerRole();
if (!workflowOwnerRole) {
throw new Error(`Failed to find owner workflow role. ${UM_FIX_INSTRUCTION}`);
throw new ApplicationError(`Failed to find owner workflow role. ${UM_FIX_INSTRUCTION}`);
}
return workflowOwnerRole;
@ -255,7 +255,9 @@ export class SourceControlImportService {
['id'],
);
if (upsertResult?.identifiers?.length !== 1) {
throw new Error(`Failed to upsert workflow ${importedWorkflow.id ?? 'new'}`);
throw new ApplicationError('Failed to upsert workflow', {
extra: { workflowId: importedWorkflow.id ?? 'new' },
});
}
// Update workflow owner to the user who exported the workflow, if that user exists
// in the instance, and the workflow doesn't already have an owner
@ -435,7 +437,7 @@ export class SourceControlImportService {
select: ['id'],
});
if (findByName && findByName.id !== tag.id) {
throw new Error(
throw new ApplicationError(
`A tag with the name <strong>${tag.name}</strong> already exists locally.<br />Please either rename the local tag, or the remote one with the id <strong>${tag.id}</strong> in the tags.json file.`,
);
}

View file

@ -10,7 +10,7 @@ import {
sourceControlFoldersExistCheck,
} from './sourceControlHelper.ee';
import { InstanceSettings } from 'n8n-core';
import { jsonParse } from 'n8n-workflow';
import { ApplicationError, jsonParse } from 'n8n-workflow';
import {
SOURCE_CONTROL_SSH_FOLDER,
SOURCE_CONTROL_GIT_FOLDER,
@ -150,7 +150,9 @@ export class SourceControlPreferencesService {
validationError: { target: false },
});
if (validationResult.length > 0) {
throw new Error(`Invalid source control preferences: ${JSON.stringify(validationResult)}`);
throw new ApplicationError('Invalid source control preferences', {
extra: { preferences: validationResult },
});
}
return validationResult;
}
@ -177,7 +179,7 @@ export class SourceControlPreferencesService {
loadOnStartup: true,
});
} catch (error) {
throw new Error(`Failed to save source control preferences: ${(error as Error).message}`);
throw new ApplicationError('Failed to save source control preferences', { cause: error });
}
}
return this.sourceControlPreferences;

View file

@ -1,6 +1,6 @@
import { validate as jsonSchemaValidate } from 'jsonschema';
import type { IWorkflowBase, JsonObject, ExecutionStatus } from 'n8n-workflow';
import { jsonParse, Workflow, WorkflowOperationError } from 'n8n-workflow';
import { ApplicationError, jsonParse, Workflow, WorkflowOperationError } from 'n8n-workflow';
import type { FindOperator } from 'typeorm';
import { In } from 'typeorm';
import { ActiveExecutions } from '@/ActiveExecutions';
@ -234,7 +234,7 @@ export class ExecutionsService {
}
if (execution.finished) {
throw new Error('The execution succeeded, so it cannot be retried.');
throw new ApplicationError('The execution succeeded, so it cannot be retried.');
}
const executionMode = 'retry';
@ -276,8 +276,9 @@ export class ExecutionsService {
})) as IWorkflowBase;
if (workflowData === undefined) {
throw new Error(
`The workflow with the ID "${workflowId}" could not be found and so the data not be loaded for the retry.`,
throw new ApplicationError(
'Workflow could not be found and so the data not be loaded for the retry.',
{ extra: { workflowId } },
);
}
@ -324,7 +325,7 @@ export class ExecutionsService {
await Container.get(ActiveExecutions).getPostExecutePromise(retriedExecutionId);
if (!executionData) {
throw new Error('The retry did not start for an unknown reason.');
throw new ApplicationError('The retry did not start for an unknown reason.');
}
return !!executionData.finished;

View file

@ -3,13 +3,13 @@
import { isObjectLiteral } from '@/utils';
import { plainToInstance, instanceToPlain } from 'class-transformer';
import { validate } from 'class-validator';
import { jsonParse } from 'n8n-workflow';
import { ApplicationError, jsonParse } from 'n8n-workflow';
export class BaseFilter {
protected static async toFilter(rawFilter: string, Filter: typeof BaseFilter) {
const dto = jsonParse(rawFilter, { errorMessage: 'Failed to parse filter JSON' });
if (!isObjectLiteral(dto)) throw new Error('Filter must be an object literal');
if (!isObjectLiteral(dto)) throw new ApplicationError('Filter must be an object literal');
const instance = plainToInstance(Filter, dto, {
excludeExtraneousValues: true, // remove fields not in class
@ -25,6 +25,6 @@ export class BaseFilter {
private async validate() {
const result = await validate(this);
if (result.length > 0) throw new Error('Parsed filter does not fit the schema');
if (result.length > 0) throw new ApplicationError('Parsed filter does not fit the schema');
}
}

View file

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/naming-convention */
import { isStringArray } from '@/utils';
import { jsonParse } from 'n8n-workflow';
import { ApplicationError, jsonParse } from 'n8n-workflow';
export class BaseSelect {
static selectableFields: Set<string>;
@ -9,7 +9,7 @@ export class BaseSelect {
protected static toSelect(rawFilter: string, Select: typeof BaseSelect) {
const dto = jsonParse(rawFilter, { errorMessage: 'Failed to parse filter JSON' });
if (!isStringArray(dto)) throw new Error('Parsed select is not a string array');
if (!isStringArray(dto)) throw new ApplicationError('Parsed select is not a string array');
return dto.reduce<Record<string, true>>((acc, field) => {
if (!Select.selectableFields.has(field)) return acc;

View file

@ -1,13 +1,14 @@
import { isIntegerString } from '@/utils';
import { ApplicationError } from 'n8n-workflow';
export class Pagination {
static fromString(rawTake: string, rawSkip: string) {
if (!isIntegerString(rawTake)) {
throw new Error('Parameter take is not an integer string');
throw new ApplicationError('Parameter take is not an integer string');
}
if (!isIntegerString(rawSkip)) {
throw new Error('Parameter skip is not an integer string');
throw new ApplicationError('Parameter skip is not an integer string');
}
const [take, skip] = [rawTake, rawSkip].map((o) => parseInt(o, 10));

View file

@ -3,6 +3,7 @@ import * as ResponseHelper from '@/ResponseHelper';
import { Pagination } from './dtos/pagination.dto';
import type { ListQuery } from '@/requests';
import type { RequestHandler } from 'express';
import { ApplicationError } from 'n8n-workflow';
export const paginationListQueryMiddleware: RequestHandler = (
req: ListQuery.Request,
@ -13,7 +14,7 @@ export const paginationListQueryMiddleware: RequestHandler = (
try {
if (!rawTake && req.query.skip) {
throw new Error('Please specify `take` when using `skip`');
throw new ApplicationError('Please specify `take` when using `skip`');
}
if (!rawTake) return next();

View file

@ -3,7 +3,7 @@ import config from '@/config';
import { caching } from 'cache-manager';
import type { MemoryCache } from 'cache-manager';
import type { RedisCache } from 'cache-manager-ioredis-yet';
import { jsonStringify } from 'n8n-workflow';
import { ApplicationError, jsonStringify } from 'n8n-workflow';
import { getDefaultRedisClient, getRedisPrefix } from './redis/RedisServiceHelper';
import EventEmitter from 'events';
@ -161,7 +161,9 @@ export class CacheService extends EventEmitter {
this.emit(this.metricsCounterEvents.cacheUpdate);
const refreshValues: unknown[] = await options.refreshFunctionMany(keys);
if (keys.length !== refreshValues.length) {
throw new Error('refreshFunctionMany must return the same number of values as keys');
throw new ApplicationError(
'refreshFunctionMany must return the same number of values as keys',
);
}
const newKV: Array<[string, unknown]> = [];
for (let i = 0; i < keys.length; i++) {
@ -191,7 +193,7 @@ export class CacheService extends EventEmitter {
}
if (this.isRedisCache()) {
if (!(this.cache as RedisCache)?.store?.isCacheable(value)) {
throw new Error('Value is not cacheable');
throw new ApplicationError('Value is not cacheable');
}
}
await this.cache?.store.set(key, value, ttl);
@ -215,7 +217,7 @@ export class CacheService extends EventEmitter {
if (this.isRedisCache()) {
nonNullValues.forEach(([_key, value]) => {
if (!(this.cache as RedisCache)?.store?.isCacheable(value)) {
throw new Error('Value is not cacheable');
throw new ApplicationError('Value is not cacheable');
}
});
}
@ -301,7 +303,7 @@ export class CacheService extends EventEmitter {
}
return map;
}
throw new Error(
throw new ApplicationError(
'Keys and values do not match, this should not happen and appears to result from some cache corruption.',
);
}

View file

@ -5,7 +5,7 @@ import { Service } from 'typedi';
import { promisify } from 'util';
import axios from 'axios';
import type { PublicInstalledPackage } from 'n8n-workflow';
import { ApplicationError, type PublicInstalledPackage } from 'n8n-workflow';
import { InstanceSettings } from 'n8n-core';
import type { PackageDirectoryLoader } from 'n8n-core';
@ -93,10 +93,10 @@ export class CommunityPackagesService {
}
parseNpmPackageName(rawString?: string): CommunityPackages.ParsedPackageName {
if (!rawString) throw new Error(PACKAGE_NAME_NOT_PROVIDED);
if (!rawString) throw new ApplicationError(PACKAGE_NAME_NOT_PROVIDED);
if (INVALID_OR_SUSPICIOUS_PACKAGE_NAME.test(rawString)) {
throw new Error('Package name must be a single word');
throw new ApplicationError('Package name must be a single word');
}
const scope = rawString.includes('/') ? rawString.split('/')[0] : undefined;
@ -104,7 +104,7 @@ export class CommunityPackagesService {
const packageNameWithoutScope = scope ? rawString.replace(`${scope}/`, '') : rawString;
if (!packageNameWithoutScope.startsWith(NODE_PACKAGE_PREFIX)) {
throw new Error(`Package name must start with ${NODE_PACKAGE_PREFIX}`);
throw new ApplicationError(`Package name must start with ${NODE_PACKAGE_PREFIX}`);
}
const version = packageNameWithoutScope.includes('@')
@ -155,12 +155,12 @@ export class CommunityPackagesService {
};
Object.entries(map).forEach(([npmMessage, n8nMessage]) => {
if (errorMessage.includes(npmMessage)) throw new Error(n8nMessage);
if (errorMessage.includes(npmMessage)) throw new ApplicationError(n8nMessage);
});
this.logger.warn('npm command failed', { errorMessage });
throw new Error(PACKAGE_FAILED_TO_INSTALL);
throw new ApplicationError(PACKAGE_FAILED_TO_INSTALL);
}
}
@ -327,7 +327,7 @@ export class CommunityPackagesService {
await this.executeNpmCommand(command);
} catch (error) {
if (error instanceof Error && error.message === RESPONSE_ERROR_MESSAGES.PACKAGE_NOT_FOUND) {
throw new Error(`The npm package "${packageName}" could not be found.`);
throw new ApplicationError('npm package not found', { extra: { packageName } });
}
throw error;
}
@ -341,7 +341,7 @@ export class CommunityPackagesService {
try {
await this.executeNpmCommand(removeCommand);
} catch {}
throw new Error(RESPONSE_ERROR_MESSAGES.PACKAGE_LOADING_FAILED, { cause: error });
throw new ApplicationError(RESPONSE_ERROR_MESSAGES.PACKAGE_LOADING_FAILED, { cause: error });
}
if (loader.loadedNodes.length > 0) {
@ -354,7 +354,10 @@ export class CommunityPackagesService {
await this.loadNodesAndCredentials.postProcessLoaders();
return installedPackage;
} catch (error) {
throw new Error(`Failed to save installed package: ${packageName}`, { cause: error });
throw new ApplicationError('Failed to save installed package', {
extra: { packageName },
cause: error,
});
}
} else {
// Remove this package since it contains no loadable nodes
@ -363,7 +366,7 @@ export class CommunityPackagesService {
await this.executeNpmCommand(removeCommand);
} catch {}
throw new Error(RESPONSE_ERROR_MESSAGES.PACKAGE_DOES_NOT_CONTAIN_NODES);
throw new ApplicationError(RESPONSE_ERROR_MESSAGES.PACKAGE_DOES_NOT_CONTAIN_NODES);
}
}
}

View file

@ -16,7 +16,7 @@ import type {
INodeParameters,
INodeTypeNameVersion,
} from 'n8n-workflow';
import { Workflow, RoutingNode } from 'n8n-workflow';
import { Workflow, RoutingNode, ApplicationError } from 'n8n-workflow';
import { NodeExecuteFunctions } from 'n8n-core';
import { NodeTypes } from '@/NodeTypes';
@ -57,8 +57,9 @@ export class DynamicNodeParametersService {
// requiring a baseURL to be defined can at least not a random server be called.
// In the future this code has to get improved that it does not use the request information from
// the request rather resolves it via the parameter-path and nodeType data.
throw new Error(
`The node-type "${nodeType.description.name}" does not exist or does not have "requestDefaults.baseURL" defined!`,
throw new ApplicationError(
'Node type does not exist or does not have "requestDefaults.baseURL" defined!',
{ tags: { nodeType: nodeType.description.name } },
);
}
@ -114,7 +115,7 @@ export class DynamicNodeParametersService {
}
if (!Array.isArray(optionsData)) {
throw new Error('The returned data is not an array!');
throw new ApplicationError('The returned data is not an array');
}
return optionsData[0].map((item) => item.json) as unknown as INodePropertyOptions[];
@ -182,9 +183,10 @@ export class DynamicNodeParametersService {
) {
const method = nodeType.methods?.[type]?.[methodName];
if (typeof method !== 'function') {
throw new Error(
`The node-type "${nodeType.description.name}" does not have the method "${methodName}" defined!`,
);
throw new ApplicationError('Node type does not have method defined', {
tags: { nodeType: nodeType.description.name },
extra: { methodName },
});
}
return method;
}

View file

@ -7,6 +7,7 @@ import { UserService } from './user.service';
import type { Credentials, ListQuery } from '@/requests';
import type { Role } from '@db/entities/Role';
import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
import { ApplicationError } from 'n8n-workflow';
@Service()
export class OwnershipService {
@ -27,7 +28,7 @@ export class OwnershipService {
const workflowOwnerRole = await this.roleService.findWorkflowOwnerRole();
if (!workflowOwnerRole) throw new Error('Failed to find workflow owner role');
if (!workflowOwnerRole) throw new ApplicationError('Failed to find workflow owner role');
const sharedWorkflow = await this.sharedWorkflowRepository.findOneOrFail({
where: { workflowId, roleId: workflowOwnerRole.id },

View file

@ -1,7 +1,7 @@
import type express from 'express';
import Container, { Service } from 'typedi';
import type { User } from '@db/entities/User';
import { jsonParse } from 'n8n-workflow';
import { ApplicationError, jsonParse } from 'n8n-workflow';
import { getServiceProviderInstance } from './serviceProvider.ee';
import type { SamlUserAttributes } from './types/samlUserAttributes';
import { isSsoJustInTimeProvisioningEnabled } from '../ssoHelpers';
@ -93,7 +93,7 @@ export class SamlService {
validate: async (response: string) => {
const valid = await validateResponse(response);
if (!valid) {
throw new Error('Invalid SAML response');
throw new ApplicationError('Invalid SAML response');
}
},
});
@ -101,7 +101,7 @@ export class SamlService {
getIdentityProviderInstance(forceRecreate = false): IdentityProviderInstance {
if (this.samlify === undefined) {
throw new Error('Samlify is not initialized');
throw new ApplicationError('Samlify is not initialized');
}
if (this.identityProviderInstance === undefined || forceRecreate) {
this.identityProviderInstance = this.samlify.IdentityProvider({
@ -114,7 +114,7 @@ export class SamlService {
getServiceProviderInstance(): ServiceProviderInstance {
if (this.samlify === undefined) {
throw new Error('Samlify is not initialized');
throw new ApplicationError('Samlify is not initialized');
}
return getServiceProviderInstance(this._samlPreferences, this.samlify);
}
@ -225,7 +225,7 @@ export class SamlService {
} else if (prefs.metadata) {
const validationResult = await validateMetadata(prefs.metadata);
if (!validationResult) {
throw new Error('Invalid SAML metadata');
throw new ApplicationError('Invalid SAML metadata');
}
}
this.getIdentityProviderInstance(true);

View file

@ -11,7 +11,7 @@ import type {
WorkflowWithSharingsAndCredentials,
} from './workflows.types';
import { CredentialsService } from '@/credentials/credentials.service';
import { NodeOperationError } from 'n8n-workflow';
import { ApplicationError, NodeOperationError } from 'n8n-workflow';
import { RoleService } from '@/services/role.service';
import Container from 'typedi';
import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
@ -161,7 +161,9 @@ export class EEWorkflowsService extends WorkflowsService {
if (credentialId === undefined) return;
const matchedCredential = allowedCredentials.find(({ id }) => id === credentialId);
if (!matchedCredential) {
throw new Error('The workflow contains credentials that you do not have access to');
throw new ApplicationError(
'The workflow contains credentials that you do not have access to',
);
}
});
});

View file

@ -51,10 +51,7 @@ describe('WorkflowCredentials', () => {
});
test('Should return an error if credentials cannot be found in the DB', async () => {
const credentials = notFoundNode.credentials!.test;
const expectedError = new Error(
`Could not find credentials for type "test" with ID "${credentials.id}".`,
);
const expectedError = new Error('Could not find credential.');
await expect(WorkflowCredentials([notFoundNode])).rejects.toEqual(expectedError);
expect(credentialsRepository.findOneBy).toHaveBeenCalledTimes(1);
});