Необработанное нарушение PG::UniqueViolation для поля external_id темы

Выполнение API-запроса к маршруту /posts.json со свойством external_id, установленным в значение external_id существующей темы, вызывает ошибку, аналогичную следующей:

ActiveRecord::RecordNotUnique (PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "index_topics_on_external_id" DETAIL: Key (external_id)=(obCopying text to the system clipboard) already exists.)

Когда запрос выполняется с помощью библиотеки Discourse API, ответ представляет собой просто HTML-строку:

#<DiscourseApi::Error:"<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"utf-8\" />\n ...

Если запрос выполняется каким-либо другим способом, например с помощью HTTParty, объект ответа выглядит следующим образом:

response = HTTParty.post(url, body:, headers:)
add_note_to_db(title, response)
# Пытаемся получить объект `response` из ответа
p response.response

# Вывод:
#<Net::HTTPInternalServerError 500 Internal Server Error readbody=true>

С этим мало что можно сделать.

Ниже приведены вызовы, которые вызывают эту проблему:

lib/topic_creator.rb:237:in `save_topic'
lib/topic_creator.rb:58:in `create'
lib/post_creator.rb:490:in `create_topic'
lib/post_creator.rb:190:in `block in create'
lib/post_creator.rb:390:in `block in transaction'
lib/post_creator.rb:390:in `transaction'
lib/post_creator.rb:188:in `create'
lib/new_post_manager.rb:318:in `perform_create_post'
lib/new_post_manager.rb:252:in `perform'
app/controllers/posts_controller.rb:210:in `block in create'
lib/distributed_memoizer.rb:16:in `block in memoize'
lib/distributed_mutex.rb:53:in `block in synchronize'
lib/distributed_mutex.rb:49:in `synchronize'
lib/distributed_mutex.rb:49:in `synchronize'
lib/distributed_mutex.rb:34:in `synchronize'
lib/distributed_memoizer.rb:12:in `memoize'
app/controllers/posts_controller.rb:209:in `create'

Мне кажется, что в TopicCreator#create необходимо выбрасывать ошибку, перехватывать её в блоке транзакции в PostCreator#create, а затем обрабатывать в контроллере постов.

Идеальное решение с моей точки зрения — возвращать ошибку, указывающую, какая тема использует данный внешний ID.

Выбросить ошибку в TopicCreator#create:

  def create
    topic = Topic.new(setup_topic_params)

    if topic.external_id.present? && Topic.with_deleted.exists?(external_id: topic.external_id)
      existing_topic = Topic.with_deleted.find_by(external_id: topic.external_id)
      raise DuplicateExternalIdError.new(existing_topic), "External ID must be unique. Existing topic ID: #{existing_topic.id}"
    end
#...

# Вероятно, существует класс ошибки, который мог бы обработать это, но размещение этого в конце класса topic_creator работает для меня:

   class DuplicateExternalIdError < StandardError
    attr_reader :topic

    def initialize(topic)
      @topic = topic
    end
  end

Перехватить её в PostCreator#create:

  def create
    if valid?
      begin
        transaction do
          build_post_stats
          create_topic # именно здесь происходит ошибка
          # ...
        end
      rescue TopicCreator::DuplicateExternalIdError => e
        @errors.add(:base, "External ID must be unique. Existing topic ID: #{e.topic.id}")
      end
    end

Обработать её в контроллере постов:

# posts_controller.rb

  def handle_duplicate_external_id_error(exception)
    render json: { error: "External ID must be unique. Existing topic ID: #{exception.topic.id}" }, status: :unprocessable_entity
  end

Возможно, это решение слишком специфично для моего случая использования. Оно связано с проблемой внешних ID и дублирующихся тем, о которой упоминается здесь: API topic's external_ID can't be reused after deleting a topic and creating a new one - #2 by simon. В идеале приложение, отправляющее данные через API Discourse, могло бы использовать сообщение об ошибке для восстановления (undelete) и обновления темы, которая уже использует этот внешний ID.

1 лайк

Да, я с радостью приму PR для улучшения этого сообщения об ошибке!

Отлично! Я посмотрю, что смогу сделать.

Вероятно, это не должно быть настолько конкретным, как я предлагал вчера:

rescue TopicCreator::DuplicateExternalIdError => e
   @errors.add(:base, "Внешний ID должен быть уникальным. Существующий ID темы: #{e.topic.id}")

Достаточно просто получить ответ с деталями о том, какое именно ограничение было нарушено.