From 83489c63c833cb1be393f86af6c54631b1e7c4c1 Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Wed, 2 Sep 2026 15:44:02 +0300 Subject: [PATCH] fix(subscriber): don't let a raising callback kill event dispatch `Client::Subscriber` dispatches every CDP event on one thread and its loop had no rescue, so the first exception raised by a callback terminated that thread for the rest of the process. Nothing was dispatched after it: targets stopped being registered and every page created later raised `NoSuchTargetError`, pages already open lost their execution contexts. Callbacks that issue CDP commands make this reachable on any slow machine, a `TimeoutError` from the default dialog handler or from a network interception block is enough. Each callback is now called with a rescue around it, so the error is reported to stderr and the remaining callbacks and events still run. Refs #470 --- CHANGELOG.md | 3 +++ lib/ferrum/client/subscriber.rb | 5 +++++ spec/client_spec.rb | 22 ++++++++++++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 spec/client_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 4578991a..6f477cd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,9 @@ the browser; a failed `IO.close` is raised to the caller. ### Fixed +- A raising callback killed `Client::Subscriber`'s dispatch thread and with it every event for the rest of the + process: pages created later raised `NoSuchTargetError`, open ones lost their execution contexts. Callbacks are + now rescued one by one and reported to stderr [#470] - `create_page` raised `Failed to find browser context with id` against a browser that came up with its own startup window (browserless). That window lives in the browser's implicit context, which Chrome below 145 won't address by id. Ferrum now detects it and exposes as `Ferrum::Context#implicit?`. It's never diff --git a/lib/ferrum/client/subscriber.rb b/lib/ferrum/client/subscriber.rb index 263bd996..4e973f70 100644 --- a/lib/ferrum/client/subscriber.rb +++ b/lib/ferrum/client/subscriber.rb @@ -133,6 +133,11 @@ def call(message) @on[event]&.each_with_index do |block, index| # In case of multiple callbacks we provide current index and total block.call(params, index, total) + rescue StandardError => e + # A raising callback used to terminate the dispatch thread, and with it every + # event for the rest of the browser's life: targets stopped being registered + # and any page created later raised `NoSuchTargetError`. Report and carry on. + warn("Ferrum: #{event} callback raised #{e.class}: #{e.message}\n #{e.backtrace&.first}") end end end diff --git a/spec/client_spec.rb b/spec/client_spec.rb new file mode 100644 index 00000000..34e3de39 --- /dev/null +++ b/spec/client_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +describe Ferrum::Client do + describe "event callbacks" do + let(:remote) { Ferrum::Browser.new(base_url: base_url, timeout: 3, protocol_timeout: 3) } + + after { remote.quit } + + it "keeps dispatching events when a callback raises" do + page = remote.create_page + + expect do + remote.client.on("Target.targetCreated") { raise Ferrum::TimeoutError } + + 2.times { remote.create_page } + page.go_to("/") + end.to output(/Target.targetCreated callback raised Ferrum::TimeoutError/).to_stderr + + expect(page.body).to include("Hello world!") + end + end +end