From 8fa4e8cc0fe760b0da1eeee5cfe746189c48740b Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Tue, 30 Jun 2026 11:01:31 +0200 Subject: [PATCH] fix(Email Trigger (IMAP) Node): Surface connection error details in activation errors (#32913) --- .../__tests__/active-workflow-manager.test.ts | 30 +++++++++++++++++++ packages/cli/src/active-workflow-manager.ts | 2 +- .../test/v2/econnresetHandling.test.ts | 15 +++++++++- .../EmailReadImap/v2/EmailReadImapV2.node.ts | 7 ++++- .../src/errors/workflow-activation.error.ts | 6 ++++ .../errors/workflow-activation.error.test.ts | 15 ++++++++++ 6 files changed, 72 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/__tests__/active-workflow-manager.test.ts b/packages/cli/src/__tests__/active-workflow-manager.test.ts index 1b3337a8e64..87bcb5ad5a0 100644 --- a/packages/cli/src/__tests__/active-workflow-manager.test.ts +++ b/packages/cli/src/__tests__/active-workflow-manager.test.ts @@ -547,6 +547,36 @@ describe('ActiveWorkflowManager', () => { expect(executeErrorWorkflowSpy).toHaveBeenCalled(); expect(addQueuedWorkflowActivationSpy).toHaveBeenCalledWith(activation, workflowData); }); + + test('wraps the cause in a WorkflowActivationError that surfaces the cause message in `description`', () => { + const workflowData = mock({ id: 'wf-1', name: 'Test Workflow' }); + const additionalData = mock(); + const mode: WorkflowExecuteMode = 'trigger'; + const activation: WorkflowActivateMode = 'activate'; + const workflow = mock({ name: 'Test Workflow' }); + const node = mock({ name: 'Trigger Node' }); + const triggerError = new Error('IMAP connection closed unexpectedly'); + + const executeErrorWorkflowSpy = vi + .spyOn(activeWorkflowManager, 'executeErrorWorkflow') + .mockImplementation(() => {}); + + const getTriggerFunctions = activeWorkflowManager.getExecuteTriggerFunctions( + workflowData, + additionalData, + mode, + activation, + async () => workflowData, + ); + const context = getTriggerFunctions(workflow, node, additionalData, mode, activation); + + context.emitError(triggerError); + + const wrappedError = executeErrorWorkflowSpy.mock.calls[0][0] as WorkflowActivationError; + expect(wrappedError).toBeInstanceOf(WorkflowActivationError); + expect(wrappedError.message).not.toBe(triggerError.message); + expect(wrappedError.description).toBe('IMAP connection closed unexpectedly'); + }); }); describe('saveFailedExecution', () => { diff --git a/packages/cli/src/active-workflow-manager.ts b/packages/cli/src/active-workflow-manager.ts index c984ee2c3e3..4a93468e469 100644 --- a/packages/cli/src/active-workflow-manager.ts +++ b/packages/cli/src/active-workflow-manager.ts @@ -367,7 +367,7 @@ export class ActiveWorkflowManager { void this.activeWorkflowTriggers.remove(failedWorkflowData.id); void this.activationErrorsService.register(failedWorkflowData.id, error.message); const activationError = new WorkflowActivationError( - `There was a problem with the trigger node "${node.name}", for that reason did the workflow had to be deactivated`, + `The workflow was deactivated because its trigger node "${node.name}" failed`, { cause: error, node }, ); this.executeErrorWorkflow(activationError, failedWorkflowData, failureMode); diff --git a/packages/nodes-base/nodes/EmailReadImap/test/v2/econnresetHandling.test.ts b/packages/nodes-base/nodes/EmailReadImap/test/v2/econnresetHandling.test.ts index c8ecbf59c44..fcb6067673b 100644 --- a/packages/nodes-base/nodes/EmailReadImap/test/v2/econnresetHandling.test.ts +++ b/packages/nodes-base/nodes/EmailReadImap/test/v2/econnresetHandling.test.ts @@ -1,6 +1,7 @@ import { EventEmitter } from 'events'; import { mock } from 'vitest-mock-extended'; import type { INode, INodeTypeBaseDescription, ITriggerFunctions, IDataObject } from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; import { type ICredentialsDataImap } from '@credentials/Imap.credentials'; @@ -99,10 +100,22 @@ describe('ECONNRESET error handling', () => { mockConnection.emit('close', false); expect(triggerFunctions.emitError).toHaveBeenCalledWith( - expect.objectContaining({ message: 'Imap connection closed unexpectedly' }), + expect.objectContaining({ message: 'IMAP connection closed unexpectedly' }), ); }); + it('should emit a NodeOperationError with an actionable description on unexpected close', async () => { + const node = new EmailReadImapV2(baseDescription); + await node.trigger.call(triggerFunctions); + + mockConnection.emit('close', false); + + const emittedError = (triggerFunctions.emitError as Mock).mock + .calls[0][0] as NodeOperationError; + expect(emittedError).toBeInstanceOf(NodeOperationError); + expect(emittedError.description).toContain('Force Reconnect'); + }); + it('should call emitError only once when ECONNRESET is followed by close', async () => { const node = new EmailReadImapV2(baseDescription); await node.trigger.call(triggerFunctions); diff --git a/packages/nodes-base/nodes/EmailReadImap/v2/EmailReadImapV2.node.ts b/packages/nodes-base/nodes/EmailReadImap/v2/EmailReadImapV2.node.ts index bce9501f24f..da85d9b5d40 100644 --- a/packages/nodes-base/nodes/EmailReadImap/v2/EmailReadImapV2.node.ts +++ b/packages/nodes-base/nodes/EmailReadImap/v2/EmailReadImapV2.node.ts @@ -476,7 +476,12 @@ export class EmailReadImapV2 implements INodeType { this.logger.debug('Email Read Imap: Shutting down workflow - connected closed'); } else if (!errorReported) { this.logger.error('Email Read Imap: Connected closed unexpectedly'); - this.emitError(new Error('Imap connection closed unexpectedly')); + this.emitError( + new NodeOperationError(this.getNode(), 'IMAP connection closed unexpectedly', { + description: + 'The IMAP server closed the connection without reporting an error, usually because the server (or a proxy/firewall) periodically closes long-lived connections, or was temporarily unavailable. n8n will automatically retry reactivating the workflow. If this happens on a regular cycle, enable the "Force Reconnect" option with an interval shorter than that cycle, so n8n reconnects before the server does.', + }), + ); } conn.removeAllListeners(); }); diff --git a/packages/workflow/src/errors/workflow-activation.error.ts b/packages/workflow/src/errors/workflow-activation.error.ts index b0d9d496970..250e086a7e0 100644 --- a/packages/workflow/src/errors/workflow-activation.error.ts +++ b/packages/workflow/src/errors/workflow-activation.error.ts @@ -32,6 +32,12 @@ export class WorkflowActivationError extends ExecutionBaseError { this.node = node; this.workflowId = workflowId; this.message = message; + if (cause && !this.description) { + this.description = + 'description' in cause && typeof cause.description === 'string' + ? cause.description + : cause.message; + } this.setLevel(level); } diff --git a/packages/workflow/test/errors/workflow-activation.error.test.ts b/packages/workflow/test/errors/workflow-activation.error.test.ts index 5f927fe13fa..7ec518900af 100644 --- a/packages/workflow/test/errors/workflow-activation.error.test.ts +++ b/packages/workflow/test/errors/workflow-activation.error.test.ts @@ -29,4 +29,19 @@ describe('WorkflowActivationError', () => { expect(error.level).toBe('warning'); }); + + it('should surface the cause message in `description`', () => { + const error = new WorkflowActivationError('Generic wrapper message', { cause }); + + expect(error.description).toBe('Some error message'); + }); + + it("should prefer the cause's own `description` over its message", () => { + const richCause = new WorkflowActivationError('Cause message'); + richCause.description = 'Actionable detail'; + + const error = new WorkflowActivationError('Generic wrapper message', { cause: richCause }); + + expect(error.description).toBe('Actionable detail'); + }); });