From 4c36cea89d6b4a2261ff5d184af4ef76725009c4 Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma Date: Mon, 19 Jan 2026 15:08:39 +0000 Subject: [PATCH] fix: validate OpenAI API key format and improve error handling --- app/javascript/dashboard/composables/useAI.js | 54 ++++++++++++++++--- .../i18n/locale/en/integrations.json | 4 +- config/integration/apps.yml | 3 +- lib/integrations/llm_base_service.rb | 11 +++- 4 files changed, 63 insertions(+), 9 deletions(-) diff --git a/app/javascript/dashboard/composables/useAI.js b/app/javascript/dashboard/composables/useAI.js index 9076d5716..ad5b33efa 100644 --- a/app/javascript/dashboard/composables/useAI.js +++ b/app/javascript/dashboard/composables/useAI.js @@ -8,6 +8,22 @@ import { useAlert, useTrack } from 'dashboard/composables'; import { useI18n } from 'vue-i18n'; import { OPEN_AI_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; import OpenAPI from 'dashboard/api/integrations/openapi'; +import { useRoute } from 'vue-router'; + +/** + * Checks if an error message indicates an invalid API key. + * @param {string} errorMessage - The error message to check. + * @returns {boolean} True if the error is related to an invalid API key. + */ +const isInvalidApiKeyError = errorMessage => { + if (!errorMessage) return false; + const lowerMessage = errorMessage.toLowerCase(); + return ( + lowerMessage.includes('incorrect api key') || + lowerMessage.includes('invalid api key') || + lowerMessage.includes('unauthorized') + ); +}; /** * Cleans and normalizes a list of labels. @@ -31,6 +47,7 @@ export function useAI() { const store = useStore(); const getters = useStoreGetters(); const { t } = useI18n(); + const route = useRoute(); /** * Computed property for UI flags. @@ -131,6 +148,18 @@ export function useAI() { } }; + /** + * Shows an error toast for invalid API key with a link to settings. + */ + const showInvalidApiKeyError = () => { + const accountId = route.params?.accountId; + useAlert(t('INTEGRATION_SETTINGS.OPEN_AI.INVALID_API_KEY_ERROR'), { + type: 'link', + to: `/app/accounts/${accountId}/settings/integrations/openai`, + message: t('INTEGRATION_SETTINGS.OPEN_AI.GO_TO_SETTINGS'), + }); + }; + /** * Fetches label suggestions for the current conversation. * @returns {Promise} An array of suggested labels. @@ -150,7 +179,14 @@ export function useAI() { } = result; return cleanLabels(labels); - } catch { + } catch (error) { + const errorData = error.response?.data?.error; + const errorMessage = errorData || ''; + + // Show toast for invalid API key errors + if (isInvalidApiKeyError(errorMessage)) { + showInvalidApiKeyError(); + } return []; } }; @@ -173,11 +209,17 @@ export function useAI() { } = result; return generatedMessage; } catch (error) { - const errorData = error.response.data.error; - const errorMessage = - errorData?.error?.message || - t('INTEGRATION_SETTINGS.OPEN_AI.GENERATE_ERROR'); - useAlert(errorMessage); + const errorData = error.response?.data?.error; + const errorMessage = errorData || ''; + + // Check if this is an invalid API key error + if (isInvalidApiKeyError(errorMessage)) { + showInvalidApiKeyError(); + } else { + useAlert( + errorMessage || t('INTEGRATION_SETTINGS.OPEN_AI.GENERATE_ERROR') + ); + } return ''; } }; diff --git a/app/javascript/dashboard/i18n/locale/en/integrations.json b/app/javascript/dashboard/i18n/locale/en/integrations.json index 21e9e72af..1de40616e 100644 --- a/app/javascript/dashboard/i18n/locale/en/integrations.json +++ b/app/javascript/dashboard/i18n/locale/en/integrations.json @@ -184,7 +184,9 @@ "GENERATING": "Generating...", "CANCEL": "Cancel" }, - "GENERATE_ERROR": "There was an error processing the content, please verify your OpenAI API key and try again" + "GENERATE_ERROR": "There was an error processing the content, please verify your OpenAI API key and try again", + "INVALID_API_KEY_ERROR": "Your OpenAI API key is invalid.", + "GO_TO_SETTINGS": "Update key" }, "DELETE": { "BUTTON_TEXT": "Delete", diff --git a/config/integration/apps.yml b/config/integration/apps.yml index dd5c722a4..0d33220f8 100644 --- a/config/integration/apps.yml +++ b/config/integration/apps.yml @@ -46,7 +46,8 @@ openai: 'label': 'API Key', 'type': 'text', 'name': 'api_key', - 'validation': 'required', + 'validation': 'required|matches:/^sk-/', + 'validation-messages': { 'matches': 'API Key must start with sk-. Please enter a valid OpenAI API key.' }, }, { 'label': 'Show label suggestions', diff --git a/lib/integrations/llm_base_service.rb b/lib/integrations/llm_base_service.rb index ca9459fc8..e3e807de5 100644 --- a/lib/integrations/llm_base_service.rb +++ b/lib/integrations/llm_base_service.rb @@ -96,6 +96,10 @@ class Integrations::LlmBaseService end end + # User configuration errors that should not be reported to Sentry + # Only UnauthorizedError is included as it clearly indicates an invalid API key + USER_ERRORS = %w[RubyLLM::UnauthorizedError].freeze + def execute_ruby_llm_request(parsed_body) messages = parsed_body['messages'] model = parsed_body['model'] @@ -105,10 +109,15 @@ class Integrations::LlmBaseService setup_chat_with_messages(chat, messages) end rescue StandardError => e - ChatwootExceptionTracker.new(e, account: hook.account).capture_exception + # Don't report user configuration errors (invalid API key, etc.) to Sentry + ChatwootExceptionTracker.new(e, account: hook.account).capture_exception unless user_configuration_error?(e) build_error_response_from_exception(e, messages) end + def user_configuration_error?(error) + USER_ERRORS.include?(error.class.name) + end + def setup_chat_with_messages(chat, messages) apply_system_instructions(chat, messages) response = send_conversation_messages(chat, messages)