diff --git a/app/controllers/concerns/request_exception_handler.rb b/app/controllers/concerns/request_exception_handler.rb index ccab0090a..7f4e313b1 100644 --- a/app/controllers/concerns/request_exception_handler.rb +++ b/app/controllers/concerns/request_exception_handler.rb @@ -1,6 +1,12 @@ module RequestExceptionHandler extend ActiveSupport::Concern + QUERY_CANCELED_ERROR_MESSAGE_PATTERNS = [ + 'ActiveRecord::QueryCanceled', + 'PG::QueryCanceled', + 'canceling statement due to statement timeout' + ].freeze + included do rescue_from ActiveRecord::RecordInvalid, with: :render_record_invalid end @@ -18,6 +24,9 @@ module RequestExceptionHandler rescue ActionController::ParameterMissing => e log_handled_error(e) render_could_not_create_error(e.message) + rescue ActiveRecord::QueryCanceled => e + log_handled_error(e) + render_could_not_create_error(database_query_canceled_message) ensure # to address the thread variable leak issues in Puma/Thin webserver Current.reset @@ -32,7 +41,7 @@ module RequestExceptionHandler end def render_could_not_create_error(message) - render json: { error: message }, status: :unprocessable_entity + render json: { error: sanitized_error_message(message) }, status: :unprocessable_entity end def render_payment_required(message) @@ -59,4 +68,19 @@ module RequestExceptionHandler def log_handled_error(exception) logger.info("Handled error: #{exception.inspect}") end + + def sanitized_error_message(message) + return database_query_canceled_message if database_query_canceled_message?(message) + + message + end + + def database_query_canceled_message?(message) + error_message = message.to_s + QUERY_CANCELED_ERROR_MESSAGE_PATTERNS.any? { |pattern| error_message.include?(pattern) } + end + + def database_query_canceled_message + I18n.t('errors.database.query_canceled') + end end diff --git a/config/locales/en.yml b/config/locales/en.yml index 7aafbef0a..42758ad1f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -82,6 +82,8 @@ en: file_too_large: 'File exceeds the maximum allowed size' unsupported_content_type: 'File type not supported (only images and videos are allowed)' unexpected: 'An unexpected error occurred' + database: + query_canceled: 'The request took too long to complete. Please try again.' saml: feature_not_enabled: SAML feature not enabled for this account sso_not_enabled: SAML SSO is not enabled for this installation diff --git a/spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb index 9ab8d7316..766fd3b6b 100644 --- a/spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb @@ -51,6 +51,22 @@ RSpec.describe 'Conversation Messages API', type: :request do expect(json_response['error']).to eq('Validation failed: Content is too long (maximum is 150000 characters)') end + it 'returns a customer-safe error when the database query is canceled' do + message_builder = instance_double(Messages::MessageBuilder) + allow(Messages::MessageBuilder).to receive(:new).and_return(message_builder) + allow(message_builder).to receive(:perform) + .and_raise(ActiveRecord::QueryCanceled, 'PG::QueryCanceled: ERROR: canceling statement due to statement timeout') + + post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id), + params: { content: 'test-message', private: true }, + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unprocessable_entity) + expect(response.parsed_body['error']).to eq(I18n.t('errors.database.query_canceled')) + expect(response.parsed_body['error']).not_to include('PG::QueryCanceled') + end + it 'creates an outgoing text message with a specific bot sender' do agent_bot = create(:agent_bot) time_stamp = Time.now.utc.to_s