From 46ee72160d9589ee4c03debdc7d155ecd1faebeb Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Wed, 15 Apr 2026 11:23:25 +0200 Subject: [PATCH] Fix infinite recursion in iterator loop traversal when If/Else branch loops back to enclosing iterator (#19714) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix a stack overflow (Maximum call stack size exceeded) in getAllStepIdsInLoop caused by an If/Else branch inside an iterator loop pointing back to the enclosing iterator. The traversal incorrectly treated the enclosing iterator as a nested iterator, calling getAllStepIdsInLoop recursively with fresh visited sets, causing infinite recursion. Add the enclosing iterator's own ID to the skip condition in traverseSteps so back-edges from If/Else branches are handled the same way as back-edges from regular nextStepIds. Capture d’écran 2026-04-15 à 11 00
42 --- .../get-all-step-ids-in-loop.util.spec.ts | 110 ++++++++++++++++++ .../utils/get-all-step-ids-in-loop.util.ts | 2 +- 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/__tests__/get-all-step-ids-in-loop.util.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/__tests__/get-all-step-ids-in-loop.util.spec.ts index 454b34ea05..3ccc6d8b74 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/__tests__/get-all-step-ids-in-loop.util.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/__tests__/get-all-step-ids-in-loop.util.spec.ts @@ -207,6 +207,116 @@ describe('getAllStepIdsInLoop', () => { }); }); + describe('if-else branch looping back to enclosing iterator', () => { + it('should not stack overflow when an if-else branch points back to the enclosing iterator', () => { + const steps = [ + createMockIteratorStep('iterator1', [], ['step1']), + createMockCodeStep('step1', ['ifElse1']), + createMockIfElseStep('ifElse1', [ + { + id: 'branch-if', + filterGroupId: 'fg1', + nextStepIds: ['stepA'], + }, + { id: 'branch-else', nextStepIds: ['iterator1'] }, + ]), + createMockCodeStep('stepA', ['iterator1']), + ]; + + const result = getAllStepIdsInLoop({ + iteratorStepId: 'iterator1', + initialLoopStepIds: ['step1'], + steps, + }); + + expect(result).toEqual( + expect.arrayContaining(['step1', 'ifElse1', 'stepA']), + ); + expect(result).toHaveLength(3); + expect(result).not.toContain('iterator1'); + }); + + it('should handle multiple if-else branches where some loop back to iterator and others continue', () => { + const steps = [ + createMockIteratorStep('iterator1', ['exitStep'], ['step1']), + createMockCodeStep('step1', ['step2']), + createMockCodeStep('step2', ['ifElse1']), + createMockIfElseStep('ifElse1', [ + { + id: 'branch-skip', + filterGroupId: 'fg1', + nextStepIds: ['iterator1'], + }, + { + id: 'branch-create', + filterGroupId: 'fg2', + nextStepIds: ['stepCreate'], + }, + { id: 'branch-default', nextStepIds: [] }, + ]), + createMockCodeStep('stepCreate', ['stepSave']), + createMockCodeStep('stepSave', ['iterator1']), + createMockCodeStep('exitStep', []), + ]; + + const result = getAllStepIdsInLoop({ + iteratorStepId: 'iterator1', + initialLoopStepIds: ['step1'], + steps, + }); + + expect(result).toEqual( + expect.arrayContaining([ + 'step1', + 'step2', + 'ifElse1', + 'stepCreate', + 'stepSave', + ]), + ); + expect(result).toHaveLength(5); + expect(result).not.toContain('iterator1'); + expect(result).not.toContain('exitStep'); + }); + + it('should handle nested iterator inside if-else branch that also has a branch looping back to outer iterator', () => { + const steps = [ + createMockIteratorStep('outerIterator', ['exitStep'], ['ifElse1']), + createMockIfElseStep('ifElse1', [ + { + id: 'branch-nested', + filterGroupId: 'fg1', + nextStepIds: ['innerIterator'], + }, + { + id: 'branch-skip', + nextStepIds: ['outerIterator'], + }, + ]), + createMockIteratorStep( + 'innerIterator', + ['outerIterator'], + ['innerStep'], + ), + createMockCodeStep('innerStep', ['innerIterator']), + createMockCodeStep('exitStep', []), + ]; + + const result = getAllStepIdsInLoop({ + iteratorStepId: 'outerIterator', + initialLoopStepIds: ['ifElse1'], + steps, + }); + + expect(result).toEqual( + expect.arrayContaining(['ifElse1', 'innerIterator', 'innerStep']), + ); + expect(result).toHaveLength(3); + expect(result).not.toContain('outerIterator'); + expect(result).not.toContain('exitStep'); + }); + }); + describe('edge cases', () => { it('should handle empty initial loop step IDs', () => { const steps = [createMockIteratorStep('iterator1', ['step2'], [])]; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/get-all-step-ids-in-loop.util.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/get-all-step-ids-in-loop.util.ts index 600c53b03a..c3bd2c6cd5 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/get-all-step-ids-in-loop.util.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/iterator/utils/get-all-step-ids-in-loop.util.ts @@ -17,7 +17,7 @@ const traverseSteps = ({ allStepIdsInLoop: Set; }) => { for (const stepId of stepIds) { - if (visitedStepIds.has(stepId)) { + if (visitedStepIds.has(stepId) || stepId === iteratorStepId) { continue; }