mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-29 19:10:43 +08:00
fix(ai-builder): Long running prompts throw internal 401 error rather than expected complexity error (#24114)
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
Reference in New Issue
Block a user