fix(Email Trigger (IMAP) Node): Surface connection error details in activation errors (#32913)

This commit is contained in:
Elias Meire 2026-06-30 11:01:31 +02:00 committed by GitHub
parent be06f86d3d
commit 8fa4e8cc0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 72 additions and 3 deletions

View File

@ -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<WorkflowEntity>({ id: 'wf-1', name: 'Test Workflow' });
const additionalData = mock<IWorkflowExecuteAdditionalData>();
const mode: WorkflowExecuteMode = 'trigger';
const activation: WorkflowActivateMode = 'activate';
const workflow = mock<Workflow>({ name: 'Test Workflow' });
const node = mock<INode>({ 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', () => {

View File

@ -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);

View File

@ -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);

View File

@ -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();
});

View File

@ -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);
}

View File

@ -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');
});
});