fix(Execute Workflow Node): Fix 'Continue (using error output)' mode to output errors correctly (#19240)

This commit is contained in:
Jaakko Husso
2025-09-12 14:12:43 +03:00
committed by GitHub
parent d7b84747f8
commit 6ed39e8c1c
2 changed files with 85 additions and 7 deletions

View File

@@ -1,5 +1,5 @@
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import type { IExecuteFunctions, IWorkflowDataProxyData } from 'n8n-workflow'; import type { IExecuteFunctions, IWorkflowDataProxyData, INode } from 'n8n-workflow';
import { ExecuteWorkflow } from './ExecuteWorkflow.node'; import { ExecuteWorkflow } from './ExecuteWorkflow.node';
import { getWorkflowInfo } from './GenericFunctions'; import { getWorkflowInfo } from './GenericFunctions';
@@ -81,13 +81,15 @@ describe('ExecuteWorkflow', () => {
expect(result).toEqual([[{ json: { key: 'value' }, index: 0, pairedItem: { item: 0 } }]]); expect(result).toEqual([[{ json: { key: 'value' }, index: 0, pairedItem: { item: 0 } }]]);
}); });
test('should handle errors and continue on fail', async () => { test('should handle errors and continue on fail, no items, < 1.3 version', async () => {
executeFunctions.getNodeParameter executeFunctions.getNodeParameter
.mockReturnValueOnce('database') // source .mockReturnValueOnce('database') // source
.mockReturnValueOnce('each') // mode .mockReturnValueOnce('each') // mode
.mockReturnValueOnce(true) // waitForSubWorkflow .mockReturnValueOnce(true) // waitForSubWorkflow
.mockReturnValueOnce([]); // workflowInputs.schema .mockReturnValueOnce([]); // workflowInputs.schema
executeFunctions.getNode.mockReturnValue({ typeVersion: 1.2 } as INode);
(getWorkflowInfo as jest.Mock).mockRejectedValue(new Error('Test error')); (getWorkflowInfo as jest.Mock).mockRejectedValue(new Error('Test error'));
(executeFunctions.continueOnFail as jest.Mock).mockReturnValue(true); (executeFunctions.continueOnFail as jest.Mock).mockReturnValue(true);
@@ -96,6 +98,77 @@ describe('ExecuteWorkflow', () => {
expect(result).toEqual([[{ json: { error: 'Test error' }, pairedItem: { item: 0 } }]]); expect(result).toEqual([[{ json: { error: 'Test error' }, pairedItem: { item: 0 } }]]);
}); });
test('should handle errors and continue on fail, multiple items, < 1.3 version', async () => {
executeFunctions.getNodeParameter
.mockReturnValueOnce('database') // source
.mockReturnValueOnce('each') // mode
.mockReturnValueOnce(true) // waitForSubWorkflow
.mockReturnValue([]); // workflowInputs.schema
executeFunctions.getNode.mockReturnValue({ typeVersion: 1.2 } as INode);
executeFunctions.getInputData.mockReturnValueOnce([
{ json: { key: '1' } },
{ json: { key: '2' } },
{ json: { key: '3' } },
]);
(getWorkflowInfo as jest.Mock).mockRejectedValue(new Error('Test error'));
(executeFunctions.continueOnFail as jest.Mock).mockReturnValue(true);
const result = await executeWorkflow.execute.call(executeFunctions);
expect(result).toEqual([
[{ json: { error: 'Test error' }, pairedItem: { item: 0 }, metadata: undefined }],
[{ json: { error: 'Test error' }, pairedItem: { item: 1 }, metadata: undefined }],
[{ json: { error: 'Test error' }, pairedItem: { item: 2 }, metadata: undefined }],
]);
});
test('should handle errors and continue on fail, no items, >= 1.3 version', async () => {
executeFunctions.getNodeParameter
.mockReturnValueOnce('database') // source
.mockReturnValueOnce('each') // mode
.mockReturnValueOnce(true) // waitForSubWorkflow
.mockReturnValueOnce([]); // workflowInputs.schema
executeFunctions.getNode.mockReturnValue({ typeVersion: 1.3 } as INode);
(getWorkflowInfo as jest.Mock).mockRejectedValue(new Error('Test error'));
(executeFunctions.continueOnFail as jest.Mock).mockReturnValue(true);
const result = await executeWorkflow.execute.call(executeFunctions);
expect(result).toEqual([[{ json: { error: 'Test error' }, pairedItem: { item: 0 } }]]);
});
test('should handle errors and continue on fail, multiple items, >= 1.3 version', async () => {
executeFunctions.getNodeParameter
.mockReturnValueOnce('database') // source
.mockReturnValueOnce('each') // mode
.mockReturnValueOnce(true) // waitForSubWorkflow
.mockReturnValueOnce([]); // workflowInputs.schema
executeFunctions.getNode.mockReturnValue({ typeVersion: 1.3 } as INode);
executeFunctions.getInputData.mockReturnValueOnce([
{ json: { key: '1' } },
{ json: { key: '2' } },
{ json: { key: '3' } },
]);
(getWorkflowInfo as jest.Mock).mockRejectedValue(new Error('Test error'));
(executeFunctions.continueOnFail as jest.Mock).mockReturnValue(true);
const result = await executeWorkflow.execute.call(executeFunctions);
expect(result).toEqual([
[
{ json: { error: 'Test error' }, pairedItem: { item: 0 }, metadata: undefined },
{ json: { error: 'Test error' }, pairedItem: { item: 1 }, metadata: undefined },
{ json: { error: 'Test error' }, pairedItem: { item: 2 }, metadata: undefined },
],
]);
});
test('should throw error if not continuing on fail', async () => { test('should throw error if not continuing on fail', async () => {
executeFunctions.getNodeParameter executeFunctions.getNodeParameter
.mockReturnValueOnce('database') // source .mockReturnValueOnce('database') // source

View File

@@ -20,7 +20,7 @@ export class ExecuteWorkflow implements INodeType {
icon: 'fa:sign-in-alt', icon: 'fa:sign-in-alt',
iconColor: 'orange-red', iconColor: 'orange-red',
group: ['transform'], group: ['transform'],
version: [1, 1.1, 1.2], version: [1, 1.1, 1.2, 1.3],
subtitle: '={{"Workflow: " + $parameter["workflowId"]}}', subtitle: '={{"Workflow: " + $parameter["workflowId"]}}',
description: 'Execute another workflow', description: 'Execute another workflow',
defaults: { defaults: {
@@ -369,11 +369,16 @@ export class ExecuteWorkflow implements INodeType {
} }
} catch (error) { } catch (error) {
if (this.continueOnFail()) { if (this.continueOnFail()) {
if (returnData[i] === undefined) { const nodeVersion = this.getNode().typeVersion;
returnData[i] = []; // In versions < 1.3 using the "Continue (using error output)" mode
} // the node would return items in extra "error branches" instead of
// returning an array of items on the error output. These branches weren't really shown correctly on the UI.
// In the fixed >= 1.3 versions the errors are now all output into the single error output as an array of error items.
const outputIndex = nodeVersion >= 1.3 ? 0 : i;
returnData[outputIndex] ??= [];
const metadata = parseErrorMetadata(error); const metadata = parseErrorMetadata(error);
returnData[i].push({ returnData[outputIndex].push({
json: { error: error.message }, json: { error: error.message },
pairedItem: { item: i }, pairedItem: { item: i },
metadata, metadata,