Violation d'unique non gérée PG::UniqueViolation pour le champ external_id du sujet

Effectuer une requête API à la route /posts.json avec la propriété external_id définie sur l’external_id d’un sujet existant déclenche une erreur similaire à celle-ci :

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.)

Lorsque la requête est effectuée avec la gem Discourse API, la réponse n’est qu’une chaîne HTML :

#<DiscourseApi::Error:\"\u003c!DOCTYPE html\u003e\n\u003chtml lang=\"en\"\u003e\n\u003chead\u003e\n \u003cmeta charset=\"utf-8\" /\u003e\n ...

Si la requête est effectuée d’une autre manière, par exemple avec HTTParty, l’objet de réponse ressemble à ceci :

response = HTTParty.post(url, body:, headers:)
add_note_to_db(title, response)
# Essayer d'obtenir l'objet `response` à partir de la réponse
p response.response

# sorties :
# #<Net::HTTPInternalServerError 500 Internal Server Error readbody=true>

Il n’y a pas grand-chose à faire avec ça.

Voici les appels qui déclenchent le problème :

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'

Je pense qu’une erreur doit être levée dans TopicCreator#create, récupérée du bloc de transaction dans PostCreator#create, puis gérée dans le contrôleur des posts.

La solution idéale de mon point de vue serait de retourner une erreur indiquant quel sujet utilisait l’identifiant externe.

Lever une erreur dans 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
#...

# Il existe probablement une classe d'erreur existante qui pourrait gérer cela, mais coller ceci au bas de la classe topic_creator me convient :

   class DuplicateExternalIdError < StandardError
    attr_reader :topic

    def initialize(topic)
      @topic = topic
    end
  end

La récupérer dans PostCreator#create :

  def create
    if valid?
      begin
        transaction do
          build_post_stats
          create_topic # c'est ici que l'erreur se produit
          # ...
        end
      rescue TopicCreator::DuplicateExternalIdError => e
        @errors.add(:base, "External ID must be unique. Existing topic ID: #{e.topic.id}")
      end
    end

La gérer dans le contrôleur des posts :

# 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

C’est peut-être trop spécifique à mon cas d’utilisation pour être la bonne solution. C’est lié au problème des identifiants externes et des sujets dupliqués mentionné ici : API topic's external_ID can't be reused after deleting a topic and creating a new one - #2 by simon. Idéalement, une application qui publie sur l’API Discourse pourrait utiliser le message de réponse pour désarchiver et mettre à jour le sujet qui utilisait déjà l’identifiant externe.

1 « J'aime »

Oui, satisfait d’une PR pour améliorer ce message d’erreur !

Super ! Je vais voir ce que je peux faire.

Ce n’est probablement pas nécessaire d’être aussi spécifique que ce que je proposais hier :

rescue TopicCreator::DuplicateExternalIdError => e
   @errors.add(:base, "External ID must be unique. Existing topic ID: #{e.topic.id}")

Il suffirait d’obtenir une réponse avec des détails sur la contrainte qui a été violée.