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 rescue_from CustomExceptions::Inbox::LimitExceeded, with: :render_error_response 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(error) render json: { error: sanitized_error_message(error) }, 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