diff --git a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/helpers/error-handling.test.ts b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/helpers/error-handling.test.ts new file mode 100644 index 0000000000000..97aee1d943bc3 --- /dev/null +++ b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/helpers/error-handling.test.ts @@ -0,0 +1,55 @@ +import { RateLimitError } from 'openai'; +import { OpenAIError } from 'openai/error'; + +import { openAiFailedAttemptHandler, getCustomErrorMessage, isOpenAiError } from './error-handling'; + +describe('error-handling', () => { + describe('getCustomErrorMessage', () => { + it('should return the correct custom error message for known error codes', () => { + expect(getCustomErrorMessage('insufficient_quota')).toBe( + 'Insufficient quota detected. Learn more about resolving this issue', + ); + expect(getCustomErrorMessage('rate_limit_exceeded')).toBe('OpenAI: Rate limit reached'); + }); + + it('should return undefined for unknown error codes', () => { + expect(getCustomErrorMessage('unknown_error_code')).toBeUndefined(); + }); + }); + + describe('isOpenAiError', () => { + it('should return true if the error is an instance of OpenAIError', () => { + const error = new OpenAIError('Test error'); + expect(isOpenAiError(error)).toBe(true); + }); + + it('should return false if the error is not an instance of OpenAIError', () => { + const error = new Error('Test error'); + expect(isOpenAiError(error)).toBe(false); + }); + }); + + describe('openAiFailedAttemptHandler', () => { + it('should handle RateLimitError and modify the error message', () => { + const error = new RateLimitError( + 429, + { code: 'rate_limit_exceeded' }, + 'Rate limit exceeded', + {}, + ); + + try { + openAiFailedAttemptHandler(error); + } catch (e) { + expect(e).toBe(error); + expect(e.message).toBe('OpenAI: Rate limit reached'); + } + }); + + it('should throw the error if it is not a RateLimitError', () => { + const error = new Error('Test error'); + + expect(() => openAiFailedAttemptHandler(error)).not.toThrow(); + }); + }); +}); diff --git a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/helpers/error-handling.ts b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/helpers/error-handling.ts index 5cea5eaf5109b..4fbb140def424 100644 --- a/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/helpers/error-handling.ts +++ b/packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/helpers/error-handling.ts @@ -1,8 +1,9 @@ -import { OpenAIError } from 'openai/error'; import { RateLimitError } from 'openai'; +import { OpenAIError } from 'openai/error'; const errorMap: Record = { - insufficient_quota: 'OpenAI: Insufficient quota', + insufficient_quota: + 'Insufficient quota detected. Learn more about resolving this issue', rate_limit_exceeded: 'OpenAI: Rate limit reached', }; @@ -23,7 +24,10 @@ export const openAiFailedAttemptHandler = (error: any) => { const customErrorMessage = getCustomErrorMessage(errorCode); if (customErrorMessage) { - error.message = customErrorMessage; + if (error.error) { + (error.error as { message: string }).message = customErrorMessage; + error.message = customErrorMessage; + } } } diff --git a/packages/editor-ui/src/components/NodeExecutionErrorMessage.test.ts b/packages/editor-ui/src/components/NodeExecutionErrorMessage.test.ts new file mode 100644 index 0000000000000..3bc51a18e9a34 --- /dev/null +++ b/packages/editor-ui/src/components/NodeExecutionErrorMessage.test.ts @@ -0,0 +1,91 @@ +import { describe, it, expect } from 'vitest'; +import NodeExecutionErrorMessage from '@/components/NodeExecutionErrorMessage.vue'; +import { createComponentRenderer } from '@/__tests__/render'; + +const renderComponent = createComponentRenderer(NodeExecutionErrorMessage); + +describe('NodeExecutionErrorMessage', () => { + it('renders the component', () => { + const { getByTestId } = renderComponent({ + props: { + nodeName: 'Test Node', + errorMessage: 'An error occurred', + }, + }); + expect(getByTestId('sanitized-error-message')).toHaveTextContent('An error occurred'); + }); + + it('renders sanitized HTML in error message', () => { + const { getByTestId } = renderComponent({ + props: { + nodeName: 'Test Node', + errorMessage: + 'Insufficient quota detected. Learn more', + }, + }); + expect(getByTestId('sanitized-error-message')).toContainHTML( + 'Insufficient quota detected. Learn more', + ); + }); + + it('renders the link with the correct text', () => { + const { getByText } = renderComponent({ + props: { + nodeName: 'Test Node', + errorMessage: 'An error occurred', + }, + }); + expect(getByText('Open node')).toBeTruthy(); + }); + + it('renders the link with the correct data attributes', () => { + const { getByText } = renderComponent({ + props: { + nodeName: 'Test Node', + errorMessage: 'An error occurred', + }, + }); + const link = getByText('Open node'); + expect(link.getAttribute('data-action')).toBe('openNodeDetail'); + expect(link.getAttribute('data-action-parameter-node')).toBe('Test Node'); + }); + + it('does not render error message when it is not provided', () => { + const { queryByText } = renderComponent({ + props: { + nodeName: 'Test Node', + }, + }); + expect(queryByText('An error occurred')).not.toBeInTheDocument(); + }); + + it('sanitizes malicious script in error message', () => { + const { getByTestId } = renderComponent({ + props: { + nodeName: 'Test Node', + errorMessage: '', + }, + }); + expect(getByTestId('sanitized-error-message')).toContainHTML(''); + }); + + it('sanitizes malicious script in error message with nested tags', () => { + const { getByTestId } = renderComponent({ + props: { + nodeName: 'Test Node', + errorMessage: '
', + }, + }); + expect(getByTestId('sanitized-error-message')).toContainHTML('
'); + }); + + it('sanitizes malicious script in error message with script tag', () => { + const { container } = renderComponent({ + props: { + nodeName: 'Test Node', + errorMessage: '', + }, + }); + expect(container.querySelector('script')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/editor-ui/src/components/NodeExecutionErrorMessage.vue b/packages/editor-ui/src/components/NodeExecutionErrorMessage.vue index 5786495df4eb3..26db3153ec7e2 100644 --- a/packages/editor-ui/src/components/NodeExecutionErrorMessage.vue +++ b/packages/editor-ui/src/components/NodeExecutionErrorMessage.vue @@ -1,5 +1,6 @@