mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-22 12:31:57 +02:00
fix(core): Show the root cause of model errors in AI Agent and Basic LLM Chain (#41388)
This commit is contained in:
parent
0b349760f8
commit
9ceb3945e7
|
|
@ -116,6 +116,65 @@ describe('wrapLangChainParserError', () => {
|
|||
expect((wrapped as NodeOperationError).cause).toBe(original);
|
||||
});
|
||||
|
||||
it('shows the root cause of a nested connection error in the description', () => {
|
||||
const socketError = Object.assign(new Error('write EPROTO bad signature'), {
|
||||
code: 'EPROTO',
|
||||
});
|
||||
const fetchError = new TypeError('fetch failed', { cause: socketError });
|
||||
class APIConnectionError extends Error {}
|
||||
const original = new APIConnectionError('Connection error.', { cause: fetchError });
|
||||
|
||||
const wrapped = wrapLangChainParserError(original, node, undefined, {
|
||||
enrichNonParserErrors: true,
|
||||
});
|
||||
|
||||
expect(wrapped.message).toBe('Connection error.');
|
||||
expect((wrapped as NodeOperationError).description).toBe(
|
||||
'Original error: APIConnectionError. Cause: write EPROTO bad signature',
|
||||
);
|
||||
expect((wrapped as NodeOperationError).cause).toBe(original);
|
||||
});
|
||||
|
||||
it('adds the error code to the root cause when the message does not contain it', () => {
|
||||
const socketError = Object.assign(new Error('socket hang up'), { code: 'ECONNRESET' });
|
||||
const original = new Error('Connection error.', { cause: socketError });
|
||||
|
||||
const wrapped = wrapLangChainParserError(original, node, undefined, {
|
||||
enrichNonParserErrors: true,
|
||||
});
|
||||
|
||||
expect((wrapped as NodeOperationError).description).toBe(
|
||||
'Original error: Error. Cause: ECONNRESET: socket hang up',
|
||||
);
|
||||
});
|
||||
|
||||
it('shows the error code when the root cause has no message', () => {
|
||||
// Node 20+ reports a failed connection attempt as an AggregateError without a message
|
||||
const socketError = Object.assign(new AggregateError([], ''), { code: 'ECONNREFUSED' });
|
||||
const original = new Error('Connection error.', { cause: socketError });
|
||||
|
||||
const wrapped = wrapLangChainParserError(original, node, undefined, {
|
||||
enrichNonParserErrors: true,
|
||||
});
|
||||
|
||||
expect((wrapped as NodeOperationError).description).toBe(
|
||||
'Original error: Error. Cause: ECONNREFUSED',
|
||||
);
|
||||
});
|
||||
|
||||
it('removes URL query strings from the root cause', () => {
|
||||
const socketError = new Error('request to https://api.example.com/v1?key=secret123 failed');
|
||||
const original = new Error('Connection error.', { cause: socketError });
|
||||
|
||||
const wrapped = wrapLangChainParserError(original, node, undefined, {
|
||||
enrichNonParserErrors: true,
|
||||
});
|
||||
|
||||
const { description } = wrapped as NodeOperationError;
|
||||
expect(description).toContain('https://api.example.com/v1 failed');
|
||||
expect(description).not.toContain('secret123');
|
||||
});
|
||||
|
||||
it('uses constructor.name in the description for a custom subclass that inherits name "Error"', () => {
|
||||
class CustomAgentError extends Error {}
|
||||
const original = new CustomAgentError('boom');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { OutputParserException } from '@langchain/core/output_parsers';
|
||||
import { sanitizeErrorDetail } from '@n8n/utils/redaction/sanitize-error-detail';
|
||||
import type { INode } from 'n8n-workflow';
|
||||
import { BaseError, NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
|
|
@ -10,6 +11,8 @@ const LANGCHAIN_PARSER_ERROR_MESSAGES = ['Failed to parse. Text:', 'Unable to pa
|
|||
|
||||
const AGENT_FAILURE_FALLBACK_MESSAGE = 'Agent execution failed';
|
||||
|
||||
const MAX_CAUSE_DETAIL_LENGTH = 500;
|
||||
|
||||
function getErrorProperty(error: unknown, property: 'message' | 'name' | 'description') {
|
||||
if (typeof error !== 'object' || error === null || !(property in error)) return undefined;
|
||||
|
||||
|
|
@ -46,6 +49,11 @@ export function isLangChainParserError(error: unknown) {
|
|||
* underlying error class.
|
||||
*/
|
||||
export function getFailureType(error: unknown): string {
|
||||
const root = getRootCause(error);
|
||||
return resolveErrorName(root) ?? (root instanceof Error ? 'Error' : typeof root);
|
||||
}
|
||||
|
||||
function getRootCause(error: unknown): unknown {
|
||||
let current: unknown = error;
|
||||
const seen = new Set<Error>();
|
||||
while (current instanceof Error && current.cause instanceof Error) {
|
||||
|
|
@ -53,7 +61,7 @@ export function getFailureType(error: unknown): string {
|
|||
if (seen.has(current.cause)) break;
|
||||
current = current.cause;
|
||||
}
|
||||
return resolveErrorName(current) ?? (current instanceof Error ? 'Error' : typeof current);
|
||||
return current;
|
||||
}
|
||||
|
||||
function isUselessMessage(message: string | undefined, className: string | undefined): boolean {
|
||||
|
|
@ -62,6 +70,26 @@ function isUselessMessage(message: string | undefined, className: string | undef
|
|||
return message === 'Error';
|
||||
}
|
||||
|
||||
/**
|
||||
* Names the original error class and, when there is one, the deepest error in the
|
||||
* `cause` chain, for example the socket or TLS error under an SDK "Connection error.".
|
||||
*/
|
||||
function describeOriginalError(error: unknown, className: string | undefined) {
|
||||
const original = className ? `Original error: ${className}` : undefined;
|
||||
const root = getRootCause(error);
|
||||
if (root === error || !(root instanceof Error)) return original;
|
||||
|
||||
const code = 'code' in root && typeof root.code === 'string' ? root.code : undefined;
|
||||
const detail =
|
||||
code && !root.message.includes(code)
|
||||
? [code, root.message].filter(Boolean).join(': ')
|
||||
: root.message;
|
||||
if (!detail) return original;
|
||||
// The root message can come from any library, so remove secrets and URL queries.
|
||||
const cause = `Cause: ${sanitizeErrorDetail(detail, MAX_CAUSE_DETAIL_LENGTH)}`;
|
||||
return original ? `${original}. ${cause}` : cause;
|
||||
}
|
||||
|
||||
function wrapAsNodeOperationError(
|
||||
error: unknown,
|
||||
node: INode,
|
||||
|
|
@ -77,9 +105,7 @@ function wrapAsNodeOperationError(
|
|||
|
||||
const options = {
|
||||
message,
|
||||
description:
|
||||
getErrorProperty(error, 'description') ??
|
||||
(className ? `Original error: ${className}` : undefined),
|
||||
description: getErrorProperty(error, 'description') ?? describeOriginalError(error, className),
|
||||
level: 'error' as const,
|
||||
...(itemIndex !== undefined ? { itemIndex } : {}),
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user