Error no controlado PG::UniqueViolation para el campo external_id del tema

Al realizar una solicitud a la ruta /posts.json con la propiedad external_id establecida en el external_id de un tema existente, se produce un error similar a este:

ActiveRecord::RecordNotUnique (PG::UniqueViolation: ERROR: duplicate key value violates unique constraint \"index_topics_on_external_id\" DETAIL: Key (external_id)=(obCopiando texto al portapapeles del sistema) already exists.)

Cuando la solicitud se realiza con la gema Discourse API, la respuesta es solo una cadena HTML:

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

Si la solicitud se realiza de alguna otra manera, por ejemplo con HTTParty, el objeto de respuesta se ve así:

response = HTTParty.post(url, body:, headers:)
add_note_to_db(title, response)
# Intenta obtener el objeto `response` de la respuesta
p response.response

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

No hay mucho que se pueda hacer con eso.

Aquí están las llamadas que desencadenan el 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'

Creo que se necesita lanzar un error en TopicCreator#create, rescatarlo del bloque de transacción en PostCreator#create, y luego manejarlo en el controlador de posts.

La solución ideal desde mi punto de vista sería devolver un error que indicara qué tema estaba utilizando el ID externo.

Lanza un error en 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
#...

# Probablemente exista una clase de error que pueda manejar esto, pero pegar esto al final de la clase topic_creator funciona para mí:

   class DuplicateExternalIdError < StandardError
    attr_reader :topic

    def initialize(topic)
      @topic = topic
    end
  end

Rescátalo en PostCreator#create:

  def create
    if valid?
      begin
        transaction do
          build_post_stats
          create_topic # aquí es donde está ocurriendo el error
          # ...
        end
      rescue TopicCreator::DuplicateExternalIdError => e
        @errors.add(:base, "External ID must be unique. Existing topic ID: #{e.topic.id}")
      end
    end

Manéjalo en el controlador de 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

Eso es quizás demasiado específico para mi caso de uso como para ser la solución correcta. Está relacionado con el problema de los IDs externos y los temas duplicados que se menciona aquí: API topic's external_ID can't be reused after deleting a topic and creating a new one - #2 by simon. Idealmente, una aplicación que publique en la API de Discourse podría usar el mensaje de respuesta para deseliminar y actualizar el tema que ya estaba utilizando el ID externo.

1 me gusta

Sí, ¡estoy contento con una PR para mejorar este mensaje de error!

¡Genial! Veré qué puedo hacer.

Probablemente no necesite ser tan específico como lo que estaba proponiendo ayer:

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

Simplemente obtener una respuesta con detalles sobre qué restricción se violó debería ser suficiente.