# 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
87 lines
2.4 KiB
Ruby
87 lines
2.4 KiB
Ruby
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
|
|
|
|
private
|
|
|
|
def handle_with_exception
|
|
yield
|
|
rescue ActiveRecord::RecordNotFound => e
|
|
log_handled_error(e)
|
|
render_not_found_error('Resource could not be found')
|
|
rescue Pundit::NotAuthorizedError => e
|
|
log_handled_error(e)
|
|
render_unauthorized('You are not authorized to do this action')
|
|
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
|
|
end
|
|
|
|
def render_unauthorized(message)
|
|
render json: { error: message }, status: :unauthorized
|
|
end
|
|
|
|
def render_not_found_error(message)
|
|
render json: { error: message }, status: :not_found
|
|
end
|
|
|
|
def render_could_not_create_error(message)
|
|
render json: { error: sanitized_error_message(message) }, status: :unprocessable_entity
|
|
end
|
|
|
|
def render_payment_required(message)
|
|
render json: { error: message }, status: :payment_required
|
|
end
|
|
|
|
def render_internal_server_error(message)
|
|
render json: { error: message }, status: :internal_server_error
|
|
end
|
|
|
|
def render_record_invalid(exception)
|
|
log_handled_error(exception)
|
|
render json: {
|
|
message: exception.record.errors.full_messages.join(', '),
|
|
attributes: exception.record.errors.attribute_names
|
|
}, status: :unprocessable_entity
|
|
end
|
|
|
|
def render_error_response(exception)
|
|
log_handled_error(exception)
|
|
render json: exception.to_hash, status: exception.http_status
|
|
end
|
|
|
|
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
|