From 4467ebecea23837160e82d46de7da534ab96a246 Mon Sep 17 00:00:00 2001 From: Benjamin Schroth Date: Tue, 24 Mar 2026 17:45:41 +0100 Subject: [PATCH] fix(ChainLLM Node): Fix abort signal handling --- .../chains/ChainLLM/methods/chainExecutor.ts | 5 +---- .../ChainLLM/test/chainExecutor.test.ts | 22 +++++++++++++++---- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/methods/chainExecutor.ts b/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/methods/chainExecutor.ts index 13143a735303..5502c2ad7a61 100644 --- a/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/methods/chainExecutor.ts +++ b/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/methods/chainExecutor.ts @@ -108,10 +108,7 @@ async function executeSimpleChain({ const chain = prompt.pipe(llm).pipe(outputParser).withConfig(getTracingConfig(context)); // Execute the chain - const response = await chain.invoke({ - query, - signal: context.getExecutionCancelSignal(), - }); + const response = await chain.invoke({ query }, { signal: context.getExecutionCancelSignal() }); // Ensure response is always returned as an array return [response]; diff --git a/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/test/chainExecutor.test.ts b/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/test/chainExecutor.test.ts index 7e176f623a12..99b957798326 100644 --- a/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/test/chainExecutor.test.ts +++ b/packages/@n8n/nodes-langchain/nodes/chains/ChainLLM/test/chainExecutor.test.ts @@ -265,7 +265,10 @@ describe('chainExecutor', () => { expect(tracing.getTracingConfig).toHaveBeenCalledWith(mockContext); }); - it('should execute a chain with a single output parser', async () => { + it('should execute a chain with a single output parser and pass signal as config', async () => { + const abortController = new AbortController(); + mockContext.getExecutionCancelSignal.mockReturnValue(abortController.signal); + const fakeLLM = new FakeLLM({ response: 'Test response' }); const mockPromptTemplate = new PromptTemplate({ template: '{query}\n{formatInstructions}', @@ -306,6 +309,11 @@ describe('chainExecutor', () => { }); expect(result).toEqual([{ result: 'Test response' }]); + // Signal must be in the config (2nd arg), NOT bundled in the input (1st arg) + expect(mockChain.invoke).toHaveBeenCalledWith( + { query: 'Hello' }, + { signal: abortController.signal }, + ); }); it('should wrap non-array responses in an array', async () => { @@ -345,8 +353,10 @@ describe('chainExecutor', () => { expect(result).toEqual([{ result: 'Test response' }]); }); - it('should pass the execution cancel signal to the chain', async () => { - // For this test, we'll just verify that getExecutionCancelSignal is called + it('should pass the execution cancel signal as config, not as input', async () => { + const abortController = new AbortController(); + mockContext.getExecutionCancelSignal.mockReturnValue(abortController.signal); + const fakeLLM = new FakeLLM({ response: 'Test response' }); const mockPromptTemplate = new PromptTemplate({ template: '{query}', @@ -377,7 +387,11 @@ describe('chainExecutor', () => { }); expect(mockContext.getExecutionCancelSignal).toHaveBeenCalled(); - expect(mockChain.invoke).toHaveBeenCalled(); + // Signal must be in the config (2nd arg), NOT bundled in the input (1st arg) + expect(mockChain.invoke).toHaveBeenCalledWith( + { query: 'Hello' }, + { signal: abortController.signal }, + ); }); it('should support chat models', async () => {