From 2edeefcf33c96aa17fc57d1e85207f7a4df3653d Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Wed, 2 Sep 2026 21:51:50 +0300 Subject: [PATCH] feat(errors): name the command a timeout was waiting on `Ferrum::TimeoutError` said only that something timed out, so a report of one carried no hint of which command, on which session, was left unanswered. Diagnosing #470 needed full `FERRUM_DEBUG` logs from three reporters to find out it was `Page.enable`. The command and session are now part of the message and exposed as `#command`/`#session_id`. Waits that aren't for a command, network idle for instance, keep the message they had. Refs #470 --- CHANGELOG.md | 2 ++ lib/ferrum/client.rb | 2 +- lib/ferrum/errors.rb | 36 ++++++++++++++++++++++++++++++++++-- sig/ferrum/errors.rbs | 14 +++++++++++++- spec/client_spec.rb | 7 +++++++ 5 files changed, 57 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f477cd3..756bc929 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ mobile device's viewport and touch support together. [#94] ### Changed +- `Ferrum::TimeoutError` names the CDP command and session it was waiting on, and exposes them as `#command` and + `#session_id`. Waits that aren't for a command keep the old message [#470] - `Ferrum::Page::Stream#stream` now closes the CDP stream handle (`IO.close`) once it's been fully read, so streams opened for `Ferrum::Browser#pdf` and `Ferrum::Page::Tracing#record` no longer keep their backing storage alive in the browser; a failed `IO.close` is raised to the caller. diff --git a/lib/ferrum/client.rb b/lib/ferrum/client.rb index f6dd0045..ad4ff0d0 100644 --- a/lib/ferrum/client.rb +++ b/lib/ferrum/client.rb @@ -226,7 +226,7 @@ def send_message(message, async:, timeout: nil) @pendings.delete(message[:id]) raise DeadBrowserError if data.nil? && @ws.messages.closed? - raise TimeoutError unless data + raise TimeoutError.new(message[:method], session_id: message[:sessionId]) unless data error, response = data.values_at("error", "result") raise_browser_error(error) if error diff --git a/lib/ferrum/errors.rb b/lib/ferrum/errors.rb index d227c099..cb8e67cf 100644 --- a/lib/ferrum/errors.rb +++ b/lib/ferrum/errors.rb @@ -40,18 +40,50 @@ def initialize(url, pendings = []) # Raised when waiting for a response from the browser times out. class TimeoutError < Error + # The CDP method that didn't answer in time, `nil` when the wait wasn't for a command. # - # Explains that waiting for a response timed out. + # @return [String, nil] + attr_reader :command + + # The session the command was sent to, `nil` for the browser-wide session. + # + # @return [String, nil] + attr_reader :session_id + + # + # @param [String, nil] command + # The CDP method that timed out. + # + # @param [String, nil] session_id + # The session it was sent to. + # + def initialize(command = nil, session_id: nil) + @command = command + @session_id = session_id + super() + end + + # + # Explains that waiting for a response timed out, naming the command when + # there was one. # # @return [String] # def message - "Timed out waiting for response. It's possible that this happened " \ + "Timed out waiting for #{awaited}. It's possible that this happened " \ "because something took a very long time (for example a page load " \ "was slow). If so, setting the :timeout option to a higher value might " \ "help. If this happened on an internal protocol call instead, try " \ "raising :protocol_timeout." end + + private + + def awaited + return "response" unless @command + + @session_id ? "a response to #{@command} (session #{@session_id})" : "a response to #{@command}" + end end # Raised when an evaluated script takes too long to return a value. diff --git a/sig/ferrum/errors.rbs b/sig/ferrum/errors.rbs index 1fd317b0..616c7e28 100644 --- a/sig/ferrum/errors.rbs +++ b/sig/ferrum/errors.rbs @@ -31,7 +31,19 @@ module Ferrum end class TimeoutError < Error - def message: () -> "Timed out waiting for response. It's possible that this happened because something took a very long time (for example a page load was slow). If so, setting the :timeout option to a higher value might help. If this happened on an internal protocol call instead, try raising :protocol_timeout." + attr_reader command: String? + attr_reader session_id: String? + + @command: String? + @session_id: String? + + def initialize: (?String? command, ?session_id: String?) -> void + + def message: () -> String + + private + + def awaited: () -> String end class ScriptTimeoutError < Error diff --git a/spec/client_spec.rb b/spec/client_spec.rb index 34e3de39..4b1916c8 100644 --- a/spec/client_spec.rb +++ b/spec/client_spec.rb @@ -1,6 +1,13 @@ # frozen_string_literal: true describe Ferrum::Client do + describe "timeouts" do + it "names the command that timed out" do + expect { page.command("Page.navigate", timeout: 0.5, url: base_url("/really_slow")) } + .to raise_error(Ferrum::TimeoutError, /a response to Page.navigate \(session .+\)/) + end + end + describe "event callbacks" do let(:remote) { Ferrum::Browser.new(base_url: base_url, timeout: 3, protocol_timeout: 3) }