From 1987363f7941285c51fda849a4ac92832368b25a Mon Sep 17 00:00:00 2001 From: Ivan Atanasov Date: Tue, 19 Nov 2024 16:28:02 +0100 Subject: [PATCH] fix(editor): Add documentation link to insufficient quota message (#11777) --- .../OpenAi/helpers/error-handling.test.ts | 55 +++++++++++ .../vendors/OpenAi/helpers/error-handling.ts | 10 +- .../NodeExecutionErrorMessage.test.ts | 91 +++++++++++++++++++ .../components/NodeExecutionErrorMessage.vue | 4 +- 4 files changed, 156 insertions(+), 4 deletions(-) create mode 100644 packages/@n8n/nodes-langchain/nodes/vendors/OpenAi/helpers/error-handling.test.ts create mode 100644 packages/editor-ui/src/components/NodeExecutionErrorMessage.test.ts 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 0000000000..97aee1d943 --- /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 5cea5eaf51..4fbb140def 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 0000000000..3bc51a18e9 --- /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 5786495df4..26db3153ec 100644 --- a/packages/editor-ui/src/components/NodeExecutionErrorMessage.vue +++ b/packages/editor-ui/src/components/NodeExecutionErrorMessage.vue @@ -1,5 +1,6 @@