diff --git a/packages/cli/src/ActiveExecutions.ts b/packages/cli/src/ActiveExecutions.ts index 9d606aab7d..ceb74d628c 100644 --- a/packages/cli/src/ActiveExecutions.ts +++ b/packages/cli/src/ActiveExecutions.ts @@ -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) { 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, ): 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; diff --git a/packages/cli/src/ActiveWebhooks.ts b/packages/cli/src/ActiveWebhooks.ts index 0b49a07b9b..73eecd0379 100644 --- a/packages/cli/src/ActiveWebhooks.ts +++ b/packages/cli/src/ActiveWebhooks.ts @@ -6,7 +6,7 @@ import type { WorkflowActivateMode, WorkflowExecuteMode, } from 'n8n-workflow'; -import { 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 { 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); diff --git a/packages/cli/src/ActiveWorkflowRunner.ts b/packages/cli/src/ActiveWorkflowRunner.ts index 1c8d1cc7a7..966092100f 100644 --- a/packages/cli/src/ActiveWorkflowRunner.ts +++ b/packages/cli/src/ActiveWorkflowRunner.ts @@ -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({ diff --git a/packages/cli/src/CredentialTypes.ts b/packages/cli/src/CredentialTypes.ts index 354a7649fd..d97825a672 100644 --- a/packages/cli/src/CredentialTypes.ts +++ b/packages/cli/src/CredentialTypes.ts @@ -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 }, + }); } } diff --git a/packages/cli/src/CredentialsHelper.ts b/packages/cli/src/CredentialsHelper.ts index 8b6876944c..3877bbd84c 100644 --- a/packages/cli/src/CredentialsHelper.ts +++ b/packages/cli/src/CredentialsHelper.ts @@ -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 { 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) { diff --git a/packages/cli/src/Db.ts b/packages/cli/src/Db.ts index e9b06499bd..c53419ac76 100644 --- a/packages/cli/src/Db.ts +++ b/packages/cli/src/Db.ts @@ -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 } }); } } diff --git a/packages/cli/src/ExternalHooks.ts b/packages/cli/src/ExternalHooks.ts index b2e001adf2..5a17f3c2f8 100644 --- a/packages/cli/src/ExternalHooks.ts +++ b/packages/cli/src/ExternalHooks.ts @@ -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, + }); } } } diff --git a/packages/cli/src/ExternalSecrets/ExternalSecretsManager.ee.ts b/packages/cli/src/ExternalSecrets/ExternalSecretsManager.ee.ts index 74a8609563..c21499f83b 100644 --- a/packages/cli/src/ExternalSecrets/ExternalSecretsManager.ee.ts +++ b/packages/cli/src/ExternalSecrets/ExternalSecretsManager.ee.ts @@ -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.', ); } diff --git a/packages/cli/src/ExternalSecrets/providers/infisical.ts b/packages/cli/src/ExternalSecrets/providers/infisical.ts index 39a0eb92d4..0d700486a7 100644 --- a/packages/cli/src/ExternalSecrets/providers/infisical.ts +++ b/packages/cli/src/ExternalSecrets/providers/infisical.ts @@ -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 { 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]> { diff --git a/packages/cli/src/Ldap/LdapManager.ee.ts b/packages/cli/src/Ldap/LdapManager.ee.ts index d4e4a39511..8dd337e1d2 100644 --- a/packages/cli/src/Ldap/LdapManager.ee.ts +++ b/packages/cli/src/Ldap/LdapManager.ee.ts @@ -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; } diff --git a/packages/cli/src/Ldap/LdapService.ee.ts b/packages/cli/src/Ldap/LdapService.ee.ts index 216af1da33..81ca653a03 100644 --- a/packages/cli/src/Ldap/LdapService.ee.ts +++ b/packages/cli/src/Ldap/LdapService.ee.ts @@ -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( diff --git a/packages/cli/src/Ldap/LdapSync.ee.ts b/packages/cli/src/Ldap/LdapSync.ee.ts index 02b8b6d9be..bdd22559a9 100644 --- a/packages/cli/src/Ldap/LdapSync.ee.ts +++ b/packages/cli/src/Ldap/LdapSync.ee.ts @@ -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'); diff --git a/packages/cli/src/Ldap/helpers.ts b/packages/cli/src/Ldap/helpers.ts index adf66e63f0..f3e5e5a9a3 100644 --- a/packages/cli/src/Ldap/helpers.ts +++ b/packages/cli/src/Ldap/helpers.ts @@ -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 => const { valid, message } = validateLdapConfigurationSchema(ldapConfig); if (!valid) { - throw new Error(message); + throw new ApplicationError(message); } if (ldapConfig.loginEnabled && getCurrentAuthenticationMethod() === 'saml') { diff --git a/packages/cli/src/LoadNodesAndCredentials.ts b/packages/cli/src/LoadNodesAndCredentials.ts index ea98570e4f..042a15121a 100644 --- a/packages/cli/src/LoadNodesAndCredentials.ts +++ b/packages/cli/src/LoadNodesAndCredentials.ts @@ -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' ? ';' : ':'; diff --git a/packages/cli/src/NodeTypes.ts b/packages/cli/src/NodeTypes.ts index 0ce7af0578..6ab17f0d97 100644 --- a/packages/cli/src/NodeTypes.ts +++ b/packages/cli/src/NodeTypes.ts @@ -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); diff --git a/packages/cli/src/Queue.ts b/packages/cli/src/Queue.ts index e4c3ffaeea..88cad41488 100644 --- a/packages/cli/src/Queue.ts +++ b/packages/cli/src/Queue.ts @@ -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; } diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 2835b7ddbe..d3ed3d247a 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -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); } diff --git a/packages/cli/src/TestWebhooks.ts b/packages/cli/src/TestWebhooks.ts index f8a5eafab2..a889bfbd30 100644 --- a/packages/cli/src/TestWebhooks.ts +++ b/packages/cli/src/TestWebhooks.ts @@ -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) diff --git a/packages/cli/src/UserManagement/UserManagementHelper.ts b/packages/cli/src/UserManagement/UserManagementHelper.ts index 448a469a6c..1b74bf32e5 100644 --- a/packages/cli/src/UserManagement/UserManagementHelper.ts +++ b/packages/cli/src/UserManagement/UserManagementHelper.ts @@ -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 => export async function compareHash(plaintext: string, hashed: string): Promise { 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 }); } } diff --git a/packages/cli/src/UserManagement/email/UserManagementMailer.ts b/packages/cli/src/UserManagement/email/UserManagementMailer.ts index 64f72099fe..9aa8efa314 100644 --- a/packages/cli/src/UserManagement/email/UserManagementMailer.ts +++ b/packages/cli/src/UserManagement/email/UserManagementMailer.ts @@ -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; type TemplateName = 'invite' | 'passwordReset'; @@ -50,7 +51,7 @@ export class UserManagementMailer { } async verifyConnection(): Promise { - if (!this.mailer) throw new Error('No mailer configured.'); + if (!this.mailer) throw new ApplicationError('No mailer configured.'); return this.mailer.verifyConnection(); } diff --git a/packages/cli/src/WaitTracker.ts b/packages/cli/src/WaitTracker.ts index c5bc493fc6..80eb337eaf 100644 --- a/packages/cli/src/WaitTracker.ts +++ b/packages/cli/src/WaitTracker.ts @@ -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); diff --git a/packages/cli/src/WorkflowCredentials.ts b/packages/cli/src/WorkflowCredentials.ts index b05b5e18ec..f7f915d32c 100644 --- a/packages/cli/src/WorkflowCredentials.ts +++ b/packages/cli/src/WorkflowCredentials.ts @@ -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 { 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; diff --git a/packages/cli/src/auth/jwt.ts b/packages/cli/src/auth/jwt.ts index 52c57533e8..bbb6386742 100644 --- a/packages/cli/src/auth/jwt.ts +++ b/packages/cli/src/auth/jwt.ts @@ -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 { 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; } diff --git a/packages/cli/src/commands/BaseCommand.ts b/packages/cli/src/commands/BaseCommand.ts index a2a36b345a..2dc19139a3 100644 --- a/packages/cli/src/commands/BaseCommand.ts +++ b/packages/cli/src/commands/BaseCommand.ts @@ -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`.', ); } diff --git a/packages/cli/src/commands/audit.ts b/packages/cli/src/commands/audit.ts index 082aab3a1b..eb027ae0fa 100644 --- a/packages/cli/src/commands/audit.ts +++ b/packages/cli/src/commands/audit.ts @@ -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( diff --git a/packages/cli/src/commands/execute.ts b/packages/cli/src/commands/execute.ts index 042632d4ea..e33be3e4e3 100644 --- a/packages/cli/src/commands/execute.ts +++ b/packages/cli/src/commands/execute.ts @@ -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) { diff --git a/packages/cli/src/commands/executeBatch.ts b/packages/cli/src/commands/executeBatch.ts index 4af312cbab..c146bea8f5 100644 --- a/packages/cli/src/commands/executeBatch.ts +++ b/packages/cli/src/commands/executeBatch.ts @@ -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'); } }); } diff --git a/packages/cli/src/commands/export/credentials.ts b/packages/cli/src/commands/export/credentials.ts index 1004e0d617..8b2c5394d4 100644 --- a/packages/cli/src/commands/export/credentials.ts +++ b/packages/cli/src/commands/export/credentials.ts @@ -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) { diff --git a/packages/cli/src/commands/export/workflow.ts b/packages/cli/src/commands/export/workflow.ts index ab8ad60283..aceaa29bbb 100644 --- a/packages/cli/src/commands/export/workflow.ts +++ b/packages/cli/src/commands/export/workflow.ts @@ -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) { diff --git a/packages/cli/src/commands/import/credentials.ts b/packages/cli/src/commands/import/credentials.ts index 3751903038..cb4bdc6a1f 100644 --- a/packages/cli/src/commands/import/credentials.ts +++ b/packages/cli/src/commands/import/credentials.ts @@ -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; diff --git a/packages/cli/src/commands/import/workflow.ts b/packages/cli/src/commands/import/workflow.ts index 42c0158484..58aed9484d 100644 --- a/packages/cli/src/commands/import/workflow.ts +++ b/packages/cli/src/commands/import/workflow.ts @@ -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; diff --git a/packages/cli/src/commands/worker.ts b/packages/cli/src/commands/worker.ts index 0f7b0d7f15..856b4fa43b 100644 --- a/packages/cli/src/commands/worker.ts +++ b/packages/cli/src/commands/worker.ts @@ -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'); diff --git a/packages/cli/src/config/index.ts b/packages/cli/src/config/index.ts index b9635966d8..8e3fe112f6 100644 --- a/packages/cli/src/config/index.ts +++ b/packages/cli/src/config/index.ts @@ -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; } diff --git a/packages/cli/src/config/utils.ts b/packages/cli/src/config/utils.ts index 9fe2fcec85..918bd9b97e 100644 --- a/packages/cli/src/config/utils.ts +++ b/packages/cli/src/config/utils.ts @@ -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) => { - 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); diff --git a/packages/cli/src/controllers/auth.controller.ts b/packages/cli/src/controllers/auth.controller.ts index d3b1b4140c..7aaa3718ea 100644 --- a/packages/cli/src/controllers/auth.controller.ts +++ b/packages/cli/src/controllers/auth.controller.ts @@ -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 { 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; diff --git a/packages/cli/src/controllers/oauth/oAuth2Credential.controller.ts b/packages/cli/src/controllers/oauth/oAuth2Credential.controller.ts index 219ef2f844..169affb29c 100644 --- a/packages/cli/src/controllers/oauth/oAuth2Credential.controller.ts +++ b/packages/cli/src/controllers/oauth/oAuth2Credential.controller.ts @@ -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; } diff --git a/packages/cli/src/databases/dsl/Table.ts b/packages/cli/src/databases/dsl/Table.ts index a92cb68343..e132042749 100644 --- a/packages/cli/src/databases/dsl/Table.ts +++ b/packages/cli/src/databases/dsl/Table.ts @@ -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 extends LazyPromise { abstract execute(queryRunner: QueryRunner): Promise; @@ -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; diff --git a/packages/cli/src/databases/migrations/common/1700571993961-AddGlobalAdminRole.ts b/packages/cli/src/databases/migrations/common/1700571993961-AddGlobalAdminRole.ts index 5c90027dcc..c9077d79a0 100644 --- a/packages/cli/src/databases/migrations/common/1700571993961-AddGlobalAdminRole.ts +++ b/packages/cli/src/databases/migrations/common/1700571993961-AddGlobalAdminRole.ts @@ -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( diff --git a/packages/cli/src/databases/repositories/execution.repository.ts b/packages/cli/src/databases/repositories/execution.repository.ts index df1bb093bc..7bd19f4c79 100644 --- a/packages/cli/src/databases/repositories/execution.repository.ts +++ b/packages/cli/src/databases/repositories/execution.repository.ts @@ -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 { }, ) { 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') diff --git a/packages/cli/src/databases/utils/migrationHelpers.ts b/packages/cli/src/databases/utils/migrationHelpers.ts index 329cfa9b9c..c254d8c44d 100644 --- a/packages/cli/src/databases/utils/migrationHelpers.ts +++ b/packages/cli/src/databases/utils/migrationHelpers.ts @@ -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 { diff --git a/packages/cli/src/decorators/registerController.ts b/packages/cli/src/decorators/registerController.ts index f4a8bc0e79..1900aa4c7d 100644 --- a/packages/cli/src/decorators/registerController.ts +++ b/packages/cli/src/decorators/registerController.ts @@ -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 diff --git a/packages/cli/src/environments/sourceControl/sourceControl.service.ee.ts b/packages/cli/src/environments/sourceControl/sourceControl.service.ee.ts index 990e90c0bd..54133154f5 100644 --- a/packages/cli/src/environments/sourceControl/sourceControl.service.ee.ts +++ b/packages/cli/src/environments/sourceControl/sourceControl.service.ee.ts @@ -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.', ); } diff --git a/packages/cli/src/environments/sourceControl/sourceControlGit.service.ee.ts b/packages/cli/src/environments/sourceControl/sourceControlGit.service.ee.ts index c1acc02cb9..056badb834 100644 --- a/packages/cli/src/environments/sourceControl/sourceControlGit.service.ee.ts +++ b/packages/cli/src/environments/sourceControl/sourceControlGit.service.ee.ts @@ -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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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, deletedFiles?: Set): Promise { 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 { 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 { 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 { 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; diff --git a/packages/cli/src/environments/sourceControl/sourceControlImport.service.ee.ts b/packages/cli/src/environments/sourceControl/sourceControlImport.service.ee.ts index 96ae0a0886..059419273d 100644 --- a/packages/cli/src/environments/sourceControl/sourceControlImport.service.ee.ts +++ b/packages/cli/src/environments/sourceControl/sourceControlImport.service.ee.ts @@ -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 ${tag.name} already exists locally.
Please either rename the local tag, or the remote one with the id ${tag.id} in the tags.json file.`, ); } diff --git a/packages/cli/src/environments/sourceControl/sourceControlPreferences.service.ee.ts b/packages/cli/src/environments/sourceControl/sourceControlPreferences.service.ee.ts index 3b385b0b81..e96f4b000f 100644 --- a/packages/cli/src/environments/sourceControl/sourceControlPreferences.service.ee.ts +++ b/packages/cli/src/environments/sourceControl/sourceControlPreferences.service.ee.ts @@ -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; diff --git a/packages/cli/src/executions/executions.service.ts b/packages/cli/src/executions/executions.service.ts index 006481b465..605867b29a 100644 --- a/packages/cli/src/executions/executions.service.ts +++ b/packages/cli/src/executions/executions.service.ts @@ -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; diff --git a/packages/cli/src/middlewares/listQuery/dtos/base.filter.dto.ts b/packages/cli/src/middlewares/listQuery/dtos/base.filter.dto.ts index 47a7273b5b..d57def77cf 100644 --- a/packages/cli/src/middlewares/listQuery/dtos/base.filter.dto.ts +++ b/packages/cli/src/middlewares/listQuery/dtos/base.filter.dto.ts @@ -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'); } } diff --git a/packages/cli/src/middlewares/listQuery/dtos/base.select.dto.ts b/packages/cli/src/middlewares/listQuery/dtos/base.select.dto.ts index da7ba6752d..fc947fdcc8 100644 --- a/packages/cli/src/middlewares/listQuery/dtos/base.select.dto.ts +++ b/packages/cli/src/middlewares/listQuery/dtos/base.select.dto.ts @@ -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; @@ -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>((acc, field) => { if (!Select.selectableFields.has(field)) return acc; diff --git a/packages/cli/src/middlewares/listQuery/dtos/pagination.dto.ts b/packages/cli/src/middlewares/listQuery/dtos/pagination.dto.ts index a7750aee6b..1a837d159c 100644 --- a/packages/cli/src/middlewares/listQuery/dtos/pagination.dto.ts +++ b/packages/cli/src/middlewares/listQuery/dtos/pagination.dto.ts @@ -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)); diff --git a/packages/cli/src/middlewares/listQuery/pagination.ts b/packages/cli/src/middlewares/listQuery/pagination.ts index 2438292be7..cb101af8c1 100644 --- a/packages/cli/src/middlewares/listQuery/pagination.ts +++ b/packages/cli/src/middlewares/listQuery/pagination.ts @@ -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(); diff --git a/packages/cli/src/services/cache.service.ts b/packages/cli/src/services/cache.service.ts index 8bc0c563f1..ba77aea016 100644 --- a/packages/cli/src/services/cache.service.ts +++ b/packages/cli/src/services/cache.service.ts @@ -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.', ); } diff --git a/packages/cli/src/services/communityPackages.service.ts b/packages/cli/src/services/communityPackages.service.ts index 2455a885e4..53b37b99ea 100644 --- a/packages/cli/src/services/communityPackages.service.ts +++ b/packages/cli/src/services/communityPackages.service.ts @@ -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); } } } diff --git a/packages/cli/src/services/dynamicNodeParameters.service.ts b/packages/cli/src/services/dynamicNodeParameters.service.ts index d3ac8c0d6f..1788bac6e1 100644 --- a/packages/cli/src/services/dynamicNodeParameters.service.ts +++ b/packages/cli/src/services/dynamicNodeParameters.service.ts @@ -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; } diff --git a/packages/cli/src/services/ownership.service.ts b/packages/cli/src/services/ownership.service.ts index 6d46e5911f..08a3699f20 100644 --- a/packages/cli/src/services/ownership.service.ts +++ b/packages/cli/src/services/ownership.service.ts @@ -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 }, diff --git a/packages/cli/src/sso/saml/saml.service.ee.ts b/packages/cli/src/sso/saml/saml.service.ee.ts index fc170ff2ca..20ce62749e 100644 --- a/packages/cli/src/sso/saml/saml.service.ee.ts +++ b/packages/cli/src/sso/saml/saml.service.ee.ts @@ -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); diff --git a/packages/cli/src/workflows/workflows.services.ee.ts b/packages/cli/src/workflows/workflows.services.ee.ts index f865e2f382..7ebdd02b7b 100644 --- a/packages/cli/src/workflows/workflows.services.ee.ts +++ b/packages/cli/src/workflows/workflows.services.ee.ts @@ -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', + ); } }); }); diff --git a/packages/cli/test/unit/WorkflowCredentials.test.ts b/packages/cli/test/unit/WorkflowCredentials.test.ts index 6fc47cbbe0..504848dd51 100644 --- a/packages/cli/test/unit/WorkflowCredentials.test.ts +++ b/packages/cli/test/unit/WorkflowCredentials.test.ts @@ -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); });