fix: Sanitize query canceled API errors (#14933)
# Pull Request Template ## Description Message creation API failures caused by PostgreSQL query cancellations now return a generic retryable error instead of exposing raw database internals such as `PG::QueryCanceled`, tuple identifiers, or relation names to customers. Existing validation failures continue to return their specific validation messages. Closes [CW-7538](https://linear.app/chatwoot/issue/CW-7538/do-not-expose-pgquerycanceled-details-in-messages-api-errors) ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality not to work as expected) - [ ] This change requires a documentation update ## How Has This Been Tested? Reproduced the messages API path by raising `ActiveRecord::QueryCanceled` from `Messages::MessageBuilder` and verified the response contains the customer-safe localized error without `PG::QueryCanceled` details. Validation run locally: - `bundle exec rspec spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb` - `bundle exec rubocop app/controllers/concerns/request_exception_handler.rb spec/controllers/api/v1/accounts/conversations/messages_controller_spec.rb` - `git diff --check` ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user