discourse/lib/discourse_event.rb
Natalie Tay 42c4619295
FIX: Isolate DiscourseEvent handlers so one error doesn't skip the rest (#38485)
DiscourseEvent.trigger iterates handlers with .each and has no rescue —
if any handler raises, Ruby stops the loop and all subsequent handlers
are silently skipped.

This PR adds a `continue_on_error` to trigger. When enabled, each
handler is rescued individually and errors are logged via
Discourse.warn_exception, so the remaining handlers continue to run.

PostCreator#trigger_after_events (which runs outside the transaction)
now passes continue_on_error: true, replacing the outer begin/rescue
blocks from #38146 with per-handler isolation.

A logged warning looks like:
```
  on(:post_created) handler error : RuntimeError : boom
  plugins/discourse-automation/lib/automation.rb:42:in `block in ...'
  lib/discourse_event.rb:17:in `block in trigger'
  lib/discourse_event.rb:15:in `each'
  ...
  ```
2026-03-11 15:54:16 +08:00

51 lines
1.4 KiB
Ruby

# frozen_string_literal: true
# This is meant to be used by plugins to trigger and listen to events
# So we can execute code when things happen.
class DiscourseEvent
# Defaults to a hash where default values are empty sets.
def self.events
@events ||= Hash.new { |hash, key| hash[key] = Set.new }
end
def self.trigger(event_name, *args, continue_on_error: false, **kwargs)
events[event_name].each do |event|
event.call(*args, **kwargs)
rescue => e
raise unless continue_on_error
Discourse.warn_exception(e, message: "on(:#{event_name}) handler error")
end
end
def self.on(event_name, &block)
case event_name
when :user_badge_removed
Discourse.deprecate(
"The :user_badge_removed event is deprecated. Please use :user_badge_revoked instead",
since: "3.1.0.beta5",
drop_from: "3.2.0.beta1",
output_in_test: true,
)
when :post_notification_alert
Discourse.deprecate(
"The :post_notification_alert event is deprecated. Please use :push_notification instead",
since: "3.2.0.beta1",
drop_from: "3.3.0.beta1",
output_in_test: true,
)
else
# ignore
end
events[event_name] << block
end
def self.off(event_name, &block)
raise ArgumentError.new "DiscourseEvent.off must reference a block" if block.nil?
events[event_name].delete(block)
end
def self.all_off(event_name)
events.delete(event_name)
end
end