Violazione Unica PG::UniqueViolation per il campo topic external_id

Effettuare una richiesta API alla rotta /posts.json con la proprietà external_id impostata su external_id di un argomento esistente genera un errore simile a questo:

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

Quando la richiesta viene effettuata con la gemma Discourse API, la risposta è solo una stringa HTML:

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

Se la richiesta viene effettuata in altro modo, ad esempio con HTTParty, l’oggetto risposta appare così:

response = HTTParty.post(url, body:, headers:)
add_note_to_db(title, response)
# Try getting the `response` object from the response
p response.response

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

Non c’è molto che si possa fare con questo.

Ecco le chiamate che generano il problema:

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'

PENSO che un errore debba essere sollevato in TopicCreator#create, gestito dal blocco transaction in PostCreator#create, quindi gestito nel controller dei post.

La soluzione ideale dal mio punto di vista sarebbe restituire un errore che indichi quale argomento stava utilizzando l’ID esterno.

Solleva un errore in 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
#...

# C'è probabilmente una classe di errore esistente che potrebbe gestire questo, ma inserirla in fondo alla classe topic_creator funziona per me:

   class DuplicateExternalIdError < StandardError
    attr_reader :topic

    def initialize(topic)
      @topic = topic
    end
  end

Gestiscilo in PostCreator#create:

  def create
    if valid?
      begin
        transaction do
          build_post_stats
          create_topic # this is where the error is happening
          # ...
        end
      rescue TopicCreator::DuplicateExternalIdError => e
        @errors.add(:base, "External ID must be unique. Existing topic ID: #{e.topic.id}")
      end
    end

Gestiscilo nel controller dei post:

# 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

Questa è forse troppo specifica per il mio caso d’uso per essere la soluzione giusta. È correlata al problema degli ID esterni e degli argomenti duplicati menzionato qui: API topic's external_ID can't be reused after deleting a topic and creating a new one - #2 by simon. Idealmente, un’applicazione che pubblica sull’API di Discourse potrebbe utilizzare il messaggio di risposta per ripristinare e aggiornare l’argomento che stava già utilizzando l’ID esterno.

1 Mi Piace

Sì, sono soddisfatto di una PR per migliorare questo messaggio di errore!

Ottimo! Vedrò cosa posso fare.

Probabilmente non c’è bisogno di essere specifici come quello che proponevo ieri:

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

Basta ricevere una risposta con i dettagli su quale vincolo è stato violato dovrebbe essere sufficiente.