From 71ca931188d2b96d18ad9ce16123ea00f4e5338c Mon Sep 17 00:00:00 2001 From: Michael Drury Date: Tue, 13 Jan 2026 13:27:40 +0000 Subject: [PATCH] fix(ai-builder): Long running prompts throw internal 401 error rather than expected complexity error (#24114) --- .../src/test/workflow-builder-agent.test.ts | 47 ++++++++++++++++++- .../src/workflow-builder-agent.ts | 33 +++++++++++-- 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/packages/@n8n/ai-workflow-builder.ee/src/test/workflow-builder-agent.test.ts b/packages/@n8n/ai-workflow-builder.ee/src/test/workflow-builder-agent.test.ts index 78e38a0f56f..9b95628c5fa 100644 --- a/packages/@n8n/ai-workflow-builder.ee/src/test/workflow-builder-agent.test.ts +++ b/packages/@n8n/ai-workflow-builder.ee/src/test/workflow-builder-agent.test.ts @@ -197,6 +197,43 @@ describe('WorkflowBuilderAgent', () => { }).rejects.toThrow(ApplicationError); }); + it('should handle 401 expired token error from LangChain MODEL_AUTHENTICATION', async () => { + // This matches the actual error structure from LangChain when the AI assistant proxy token expires + const expiredTokenError = Object.assign(new Error('Expired token'), { + status: 401, + lc_error_code: 'MODEL_AUTHENTICATION', + }); + + mockCreateStreamProcessor.mockImplementation(() => { + // eslint-disable-next-line require-yield + return (async function* () { + throw expiredTokenError; + })(); + }); + + await expect(async () => { + const generator = agent.chat(mockPayload); + await generator.next(); + }).rejects.toThrow(ApplicationError); + }); + + it('should not treat generic 401 errors as expired token errors', async () => { + // A generic 401 without lc_error_code should be rethrown as-is, not converted to ApplicationError + const generic401Error = Object.assign(new Error('Unauthorized'), { status: 401 }); + + mockCreateStreamProcessor.mockImplementation(() => { + // eslint-disable-next-line require-yield + return (async function* () { + throw generic401Error; + })(); + }); + + await expect(async () => { + const generator = agent.chat(mockPayload); + await generator.next(); + }).rejects.toThrow(generic401Error); + }); + it('should handle invalid request errors', async () => { const invalidRequestError = Object.assign(new Error('Request failed'), { error: { @@ -207,12 +244,18 @@ describe('WorkflowBuilderAgent', () => { }, }); - (mockLlmSimple.invoke as jest.Mock).mockRejectedValue(invalidRequestError); + // Mock the stream processor to throw the invalid request error + mockCreateStreamProcessor.mockImplementation(() => { + // eslint-disable-next-line require-yield + return (async function* () { + throw invalidRequestError; + })(); + }); await expect(async () => { const generator = agent.chat(mockPayload); await generator.next(); - }).rejects.toThrow(ApplicationError); + }).rejects.toThrow(ValidationError); }); it('should rethrow unknown errors', async () => { diff --git a/packages/@n8n/ai-workflow-builder.ee/src/workflow-builder-agent.ts b/packages/@n8n/ai-workflow-builder.ee/src/workflow-builder-agent.ts index 3f3ce29a2da..346e1899f7d 100644 --- a/packages/@n8n/ai-workflow-builder.ee/src/workflow-builder-agent.ts +++ b/packages/@n8n/ai-workflow-builder.ee/src/workflow-builder-agent.ts @@ -27,6 +27,9 @@ import type { WorkflowState } from './workflow-state'; const PROMPT_IS_TOO_LARGE_ERROR = 'The current conversation and workflow state is too large to process. Try to simplify your workflow by breaking it into smaller parts.'; +const WORKFLOW_TOO_COMPLEX_ERROR = + 'Workflow generation stopped: The AI reached the maximum number of steps while building your workflow. This usually means the workflow design became too complex or got stuck in a loop while trying to create the nodes and connections.'; + /** * Type for the state snapshot with properly typed values */ @@ -283,15 +286,39 @@ export class WorkflowBuilderAgent { // If it's not an abort error, check for GraphRecursionError if (error instanceof GraphRecursionError) { - throw new ApplicationError( - 'Workflow generation stopped: The AI reached the maximum number of steps while building your workflow. This usually means the workflow design became too complex or got stuck in a loop while trying to create the nodes and connections.', - ); + throw new ApplicationError(WORKFLOW_TOO_COMPLEX_ERROR); + } + + // Check for 401 expired token errors (typically from long-running generations) + if (this.isTokenExpiredError(error)) { + throw new ApplicationError(WORKFLOW_TOO_COMPLEX_ERROR); } // Re-throw any other errors throw error; } + /** + * Checks if the error is a 401 expired token error from the LLM provider proxy. + * This typically occurs during very long-running workflow generations when + * the AI assistant service proxy token expires. + * + * We specifically check for LangChain's MODEL_AUTHENTICATION error code to ensure + * we only catch authentication errors from the LLM provider, not unrelated 401s. + */ + private isTokenExpiredError(error: unknown): boolean { + const LC_MODEL_AUTHENTICATION_ERROR = 'MODEL_AUTHENTICATION'; + + return ( + !!error && + typeof error === 'object' && + 'lc_error_code' in error && + error.lc_error_code === LC_MODEL_AUTHENTICATION_ERROR && + 'status' in error && + error.status === 401 + ); + } + private getInvalidRequestError(error: unknown): string | undefined { if ( error instanceof Error &&