From 218ce43fd67b3c425f227d85ee4bae4f205ff446 Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Wed, 19 Aug 2026 21:35:19 +0300 Subject: [PATCH 1/2] feat: add `:protocol_timeout` option for internal CDP calls and custom timeouts for `#pdf`/`#screenshot` --- CHANGELOG.md | 4 +++ lib/ferrum/browser.rb | 16 +++++++---- lib/ferrum/browser/options.rb | 4 ++- lib/ferrum/client.rb | 35 +++++++++++++++++------ lib/ferrum/context.rb | 2 +- lib/ferrum/errors.rb | 3 +- lib/ferrum/page.rb | 13 ++++++--- lib/ferrum/page/screenshot.rb | 23 +++++++++++---- sig/ferrum/browser.rbs | 4 +++ sig/ferrum/browser/options.rbs | 5 ++++ sig/ferrum/client.rbs | 10 +++---- sig/ferrum/errors.rbs | 2 +- sig/ferrum/page.rbs | 2 +- sig/ferrum/page/screenshot.rbs | 8 ++++-- spec/browser/options_spec.rb | 25 +++++++++++++++++ spec/page/screenshot_spec.rb | 1 + spec/unit/client_spec.rb | 51 ++++++++++++++++++++++++++++++++++ 17 files changed, 171 insertions(+), 37 deletions(-) create mode 100644 spec/browser/options_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index fa15b885..2edb27ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ ## [Unreleased](https://github.com/rubycdp/ferrum/compare/v0.17.2...main) ## ### Added +- `Ferrum::Browser` option `:protocol_timeout` (default 1s, independent of `:timeout`) bounds individual internal CDP + bookkeeping calls (`Target.createTarget`, `Target.attachToTarget`, etc. +- `Ferrum::Page#pdf`/`#screenshot` accept a `timeout:` argument (default 60s) for the CDP call itself. Generating a + full-page screenshot or a large PDF is a known slow outlier among CDP commands. - `Ferrum::Frame#loader_id` provides a loader id when the frame navigates [#583] - `Ferrum::Frame#lifecycle_events` provides a list of frame's events like init, networkIdle, firstPaint, etc. [#583] - `Ferrum::Frame#idle?` whether frame was loaded [#583] diff --git a/lib/ferrum/browser.rb b/lib/ferrum/browser.rb index dba34aa0..6df3fb45 100644 --- a/lib/ferrum/browser.rb +++ b/lib/ferrum/browser.rb @@ -44,7 +44,8 @@ class Browser attr_reader :client, :process, :contexts, :options - delegate %i[timeout timeout= base_url base_url= default_user_agent default_user_agent= extensions] => :options + delegate %i[timeout timeout= protocol_timeout protocol_timeout= + base_url base_url= default_user_agent default_user_agent= extensions] => :options delegate %i[command] => :client # @@ -80,13 +81,18 @@ class Browser # When present, debug output is written to this object. # # @option options [Integer, Float] :slowmo - # Set a delay in seconds to wait before sending command. - # Useful companion of headless option, so that you have time to see + # Set a delay in seconds to wait before sending a command. + # Useful companion of a headless option, so that you have time to see # changes. # # @option options [Numeric] :timeout (5) - # The number of seconds we'll wait for a response when communicating with - # browser. + # The number of seconds we'll wait for a response when communicating + # with the browser: navigations, JS evaluation, DOM queries, dispatching input, etc. + # + # @option options [Numeric] :protocol_timeout (1) + # The number of seconds we'll wait for an individual internal CDP + # bookkeeping call to respond, e.g. `Target.createTarget`, + # `Target.attachToTarget`. These normally resolve in milliseconds. # # @option options [Boolean] :js_errors # When true, JavaScript errors get re-raised in Ruby. diff --git a/lib/ferrum/browser/options.rb b/lib/ferrum/browser/options.rb index 9fe4b4d3..6b9fe13c 100644 --- a/lib/ferrum/browser/options.rb +++ b/lib/ferrum/browser/options.rb @@ -14,6 +14,7 @@ class Options WINDOW_SIZE = [1024, 768].freeze BASE_URL_SCHEMA = %w[http https].freeze DEFAULT_TIMEOUT = ENV.fetch("FERRUM_DEFAULT_TIMEOUT", 5).to_i + DEFAULT_PROTOCOL_TIMEOUT = ENV.fetch("FERRUM_PROTOCOL_TIMEOUT", 1).to_i PROCESS_TIMEOUT = ENV.fetch("FERRUM_PROCESS_TIMEOUT", 10).to_i DEBUG_MODE = !ENV.fetch("FERRUM_DEBUG", nil).nil? @@ -22,7 +23,7 @@ class Options :url, :ws_url, :env, :process_timeout, :browser_name, :browser_path, :save_path, :proxy, :port, :host, :headless, :incognito, :dockerize, :browser_options, :ignore_default_browser_options, :xvfb, :flatten - attr_accessor :timeout, :default_user_agent + attr_accessor :timeout, :protocol_timeout, :default_user_agent def initialize(options = nil) @options = Hash(options&.dup) @@ -30,6 +31,7 @@ def initialize(options = nil) @port = @options.fetch(:port, BROWSER_PORT) @host = @options.fetch(:host, BROWSER_HOST) @timeout = @options.fetch(:timeout, DEFAULT_TIMEOUT) + @protocol_timeout = @options.fetch(:protocol_timeout, DEFAULT_PROTOCOL_TIMEOUT) @window_size = @options.fetch(:window_size, WINDOW_SIZE) @js_errors = @options.fetch(:js_errors, false) @headless = @options.fetch(:headless, true) diff --git a/lib/ferrum/client.rb b/lib/ferrum/client.rb index 6f57dbb6..f6dd0045 100644 --- a/lib/ferrum/client.rb +++ b/lib/ferrum/client.rb @@ -51,12 +51,16 @@ def initialize(client, session_id) # @param [Hash] params # The command's parameters. # + # @param [Numeric, nil] timeout + # How long to wait for this command's response, overriding + # {Browser::Options#protocol_timeout}. See {Client#send_message}. + # # @return [Boolean, Hash] # `true` when sent asynchronously, otherwise the command's result. # - def command(method, async: false, **params) + def command(method, async: false, timeout: nil, **params) message = build_message(method, params) - @client.send_message(message, async: async) + @client.send_message(message, async: async, timeout: timeout) end # @@ -146,7 +150,7 @@ def event_name(event) class Client extend Forwardable - delegate %i[timeout timeout=] => :options + delegate %i[protocol_timeout protocol_timeout=] => :options attr_reader :ws_url, :options, :subscriber @@ -174,18 +178,28 @@ def initialize(ws_url, options) # @param [Hash] params # The command's parameters. # + # @param [Numeric, nil] timeout + # How long to wait for this command's response, overriding + # {Browser::Options#protocol_timeout}. See {#send_message}. + # # @return [Boolean, Hash] # `true` when sent asynchronously, otherwise the command's result. # - def command(method, async: false, **params) + def command(method, async: false, timeout: nil, **params) message = build_message(method, params) - send_message(message, async: async) + send_message(message, async: async, timeout: timeout) end # # Sends a raw CDP message over the websocket. Synchronous calls block - # until a matching response arrives, or `timeout` (delegated to - # {Browser::Options#timeout}) elapses. + # until a matching response arrives, or `timeout` elapses, defaulting to + # `protocol_timeout` (delegated to {Browser::Options#protocol_timeout}). + # That default is the transport-level budget for internal CDP bookkeeping + # (e.g. `Target.createTarget`). {Page#command} overrides + # this back to `timeout`, or a caller-supplied budget (e.g. `#pdf`/ + # `#screenshot`'s own `timeout:` argument), for the user-facing commands + # it issues -- some of which (e.g. `Page.navigate`, `Page.printToPDF`) + # rely on their own response latency to detect a stuck operation. # # @param [Hash] message # The message to send, must include an `:id` key. @@ -193,11 +207,14 @@ def command(method, async: false, **params) # @param [Boolean] async # Whether to return immediately instead of waiting for a response. # + # @param [Numeric, nil] timeout + # How long to wait for the response. Defaults to `protocol_timeout`. + # # @return [Boolean, Hash] # `true` when sent asynchronously, otherwise the parsed `"result"` # from the response. # - def send_message(message, async:) + def send_message(message, async:, timeout: nil) if async @ws.send_message(message) true @@ -205,7 +222,7 @@ def send_message(message, async:) pending = Concurrent::IVar.new @pendings[message[:id]] = pending @ws.send_message(message) - data = pending.value!(timeout) + data = pending.value!(timeout || protocol_timeout) @pendings.delete(message[:id]) raise DeadBrowserError if data.nil? && @ws.messages.closed? diff --git a/lib/ferrum/context.rb b/lib/ferrum/context.rb index 4af0fda3..b7880e59 100644 --- a/lib/ferrum/context.rb +++ b/lib/ferrum/context.rb @@ -96,7 +96,7 @@ def create_target new_pending = Concurrent::IVar.new pending = @pendings.put_if_absent(target_id, new_pending) || new_pending - resolved = pending.value(@client.timeout) + resolved = pending.value(@client.protocol_timeout) raise NoSuchTargetError unless resolved @pendings.delete(target_id) diff --git a/lib/ferrum/errors.rb b/lib/ferrum/errors.rb index 1875180f..d0e2a8bb 100644 --- a/lib/ferrum/errors.rb +++ b/lib/ferrum/errors.rb @@ -49,7 +49,8 @@ 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." + "help. If this happened on an internal protocol call instead, try " \ + "raising :protocol_timeout." end end diff --git a/lib/ferrum/page.rb b/lib/ferrum/page.rb index d0bed644..040f12cc 100644 --- a/lib/ferrum/page.rb +++ b/lib/ferrum/page.rb @@ -426,23 +426,28 @@ def activate # Whether to sleep for `Browser::Options#slowmo` seconds before sending # the command. # + # @param [Numeric, nil] timeout + # Overrides the timeout this command's response is bound by. Defaults + # to the page's `timeout`. Callers with their own budget (e.g. `#pdf`/ + # `#screenshot`) pass it explicitly. + # # @return [Hash{String => Object}] # # @example # page.command("Page.navigate", url: "https://github.com/") # - def command(method, wait: 0, slowmoable: false, **params) + def command(method, wait: 0, slowmoable: false, timeout: nil, **params) iteration = @event.reset if wait.positive? sleep(@options.slowmo) if slowmoable && @options.slowmo.positive? - result = client.command(method, **params) + result = client.command(method, timeout: timeout || self.timeout, **params) if wait.positive? # Wait a bit after command and check if iteration has - # changed which means there was some network event for + # changed, which means there was some network event for # the main frame, and it started to load new content. @event.wait(wait) if iteration != @event.iteration - set = @event.wait(timeout) + set = @event.wait(self.timeout) raise TimeoutError unless set end end diff --git a/lib/ferrum/page/screenshot.rb b/lib/ferrum/page/screenshot.rb index 3ed56ff8..cd64a2c8 100644 --- a/lib/ferrum/page/screenshot.rb +++ b/lib/ferrum/page/screenshot.rb @@ -14,6 +14,7 @@ module Screenshot DEFAULT_SCREENSHOT_FORMAT = "png" SUPPORTED_SCREENSHOT_FORMAT = %w[png jpeg jpg webp].freeze + DEFAULT_RENDER_TIMEOUT = 60 DEFAULT_PDF_OPTIONS = { landscape: false, @@ -69,6 +70,11 @@ module Screenshot # @option opts [Ferrum::RGBA] :background_color # Sets the background color. # + # @param [Numeric] timeout + # How long to wait for the screenshot to be captured. Defaults to + # {DEFAULT_RENDER_TIMEOUT} since a full-page capture is a known slow + # outlier among CDP commands. + # # @example # page.go_to("https://google.com/") # @@ -87,10 +93,10 @@ module Screenshot # @example Save with specific background color: # page.screenshot(background_color: Ferrum::RGBA.new(0, 0, 0, 0.0)) # - def screenshot(**opts) + def screenshot(timeout: DEFAULT_RENDER_TIMEOUT, **opts) path, encoding = common_options(**opts) options = screenshot_options(path, **opts) - data = capture_screenshot(options, opts[:full], opts[:background_color]) + data = capture_screenshot(options, opts[:full], opts[:background_color], timeout) return data if encoding == :base64 bin = Base64.decode64(data) @@ -128,15 +134,20 @@ def screenshot(**opts) # See other [native options](https://chromedevtools.github.io/devtools-protocol/tot/Page#method-printToPDF) you # can pass. # + # @param [Numeric] timeout + # How long to wait for the PDF to be generated. Defaults to + # {DEFAULT_RENDER_TIMEOUT} since large documents are a known slow + # outlier among CDP commands. + # # @example # page.go_to("https://google.com/") # # Save to disk as a PDF # page.pdf(path: "google.pdf", paper_width: 1.0, paper_height: 1.0) # => true # - def pdf(**opts) + def pdf(timeout: DEFAULT_RENDER_TIMEOUT, **opts) path, encoding = common_options(**opts) options = pdf_options(**opts).merge(transferMode: "ReturnAsStream") - handle = command("Page.printToPDF", **options).fetch("stream") + handle = command("Page.printToPDF", timeout: timeout, **options).fetch("stream") stream_to(path: path, encoding: encoding, handle: handle) end @@ -313,11 +324,11 @@ def to_camel_case(option) option.to_s.gsub(%r{(?:_|(/))([a-z\d]*)}) { "#{Regexp.last_match(1)}#{Regexp.last_match(2).capitalize}" }.to_sym end - def capture_screenshot(options, full, background_color) + def capture_screenshot(options, full, background_color, timeout) options = options.merge(captureBeyondViewport: true) if full with_background_color(background_color) do - command("Page.captureScreenshot", **options) + command("Page.captureScreenshot", timeout: timeout, **options) end.fetch("data") end diff --git a/sig/ferrum/browser.rbs b/sig/ferrum/browser.rbs index 43d0bf3b..4ffa780c 100644 --- a/sig/ferrum/browser.rbs +++ b/sig/ferrum/browser.rbs @@ -22,6 +22,10 @@ module Ferrum def timeout=: (::Numeric) -> ::Numeric + def protocol_timeout: () -> ::Numeric + + def protocol_timeout=: (::Numeric) -> ::Numeric + def default_user_agent: () -> String? def default_user_agent=: (String) -> String diff --git a/sig/ferrum/browser/options.rbs b/sig/ferrum/browser/options.rbs index d778c25c..87080fde 100644 --- a/sig/ferrum/browser/options.rbs +++ b/sig/ferrum/browser/options.rbs @@ -11,6 +11,8 @@ module Ferrum DEFAULT_TIMEOUT: ::Integer + DEFAULT_PROTOCOL_TIMEOUT: ::Integer + PROCESS_TIMEOUT: ::Integer DEBUG_MODE: bool @@ -19,6 +21,8 @@ module Ferrum attr_accessor timeout: ::Numeric + attr_accessor protocol_timeout: ::Numeric + attr_accessor default_user_agent: String? attr_reader logger: (IO | StringIO | nil) @@ -69,6 +73,7 @@ module Ferrum @port: String @host: String @timeout: ::Numeric + @protocol_timeout: ::Numeric @window_size: ::Array[::Integer] @js_errors: bool @headless: bool diff --git a/sig/ferrum/client.rbs b/sig/ferrum/client.rbs index 67983ac9..d75549a0 100644 --- a/sig/ferrum/client.rbs +++ b/sig/ferrum/client.rbs @@ -10,7 +10,7 @@ module Ferrum def initialize: (Client client, String session_id) -> void - def command: (String method, ?async: bool, **untyped params) -> (bool | Hash[String, untyped]) + def command: (String method, ?async: bool, ?timeout: ::Numeric?, **untyped params) -> (bool | Hash[String, untyped]) def on: (String event) { (Hash[String, untyped]) -> void } -> Integer @@ -48,9 +48,9 @@ module Ferrum def initialize: ((String | ::Addressable::URI) ws_url, Browser::Options options) -> void - def command: (String method, ?async: bool, **untyped params) -> (bool | Hash[String, untyped]) + def command: (String method, ?async: bool, ?timeout: ::Numeric?, **untyped params) -> (bool | Hash[String, untyped]) - def send_message: (Hash[Symbol, untyped] message, async: bool) -> (bool | Hash[String, untyped]) + def send_message: (Hash[Symbol, untyped] message, async: bool, ?timeout: ::Numeric?) -> (bool | Hash[String, untyped]) def on: (String event) { (Hash[String, untyped]) -> void } -> Integer @@ -66,9 +66,9 @@ module Ferrum def build_message: (String method, Hash[Symbol, untyped] params) -> Hash[Symbol, untyped] - def timeout: () -> ::Numeric + def protocol_timeout: () -> ::Numeric - def timeout=: (::Numeric value) -> ::Numeric + def protocol_timeout=: (::Numeric value) -> ::Numeric private diff --git a/sig/ferrum/errors.rbs b/sig/ferrum/errors.rbs index 393ea4f7..0afd153c 100644 --- a/sig/ferrum/errors.rbs +++ b/sig/ferrum/errors.rbs @@ -31,7 +31,7 @@ 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." + 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." end class ScriptTimeoutError < Error diff --git a/sig/ferrum/page.rbs b/sig/ferrum/page.rbs index 28fd511a..5b2531ff 100644 --- a/sig/ferrum/page.rbs +++ b/sig/ferrum/page.rbs @@ -80,7 +80,7 @@ module Ferrum def set_window_bounds: (Hash[Symbol, (::Integer | String)] bounds) -> Hash[String, untyped] - def command: (String method, ?wait: ::Numeric?, ?slowmoable: bool, **untyped) -> Hash[String, untyped] + def command: (String method, ?wait: ::Numeric?, ?slowmoable: bool, ?timeout: ::Numeric?, **untyped) -> Hash[String, untyped] def on: ((Symbol | String) event) ?{ (Hash[String, untyped]) -> void } -> Integer diff --git a/sig/ferrum/page/screenshot.rbs b/sig/ferrum/page/screenshot.rbs index 055c77b0..8159502e 100644 --- a/sig/ferrum/page/screenshot.rbs +++ b/sig/ferrum/page/screenshot.rbs @@ -1,13 +1,15 @@ module Ferrum class Page module Screenshot + DEFAULT_RENDER_TIMEOUT: ::Integer + DEFAULT_PDF_OPTIONS: { landscape: false, paper_width: ::Float, paper_height: 11, scale: ::Float } PAPER_FORMATS: { letter: { width: ::Float, height: ::Float }, legal: { width: ::Float, height: ::Float }, tabloid: { width: ::Float, height: ::Float }, ledger: { width: ::Float, height: ::Float }, :A0 => { width: ::Float, height: ::Float }, :A1 => { width: ::Float, height: ::Float }, :A2 => { width: ::Float, height: ::Float }, :A3 => { width: ::Float, height: ::Float }, :A4 => { width: ::Float, height: ::Float }, :A5 => { width: ::Float, height: ::Float }, :A6 => { width: ::Float, height: ::Float } } - def screenshot: (**untyped opts) -> untyped + def screenshot: (?timeout: ::Numeric, **untyped opts) -> untyped - def pdf: (**untyped opts) -> untyped + def pdf: (?timeout: ::Numeric, **untyped opts) -> untyped def mhtml: (?path: untyped?) -> untyped @@ -33,7 +35,7 @@ module Ferrum def to_camel_case: (untyped option) -> (:preferCSSPageSize | untyped) - def capture_screenshot: (untyped options, untyped full, untyped background_color) -> untyped + def capture_screenshot: (untyped options, untyped full, untyped background_color, ::Numeric timeout) -> untyped def maybe_resize_fullscreen: (untyped full) { () -> untyped } -> untyped diff --git a/spec/browser/options_spec.rb b/spec/browser/options_spec.rb new file mode 100644 index 00000000..b37966a8 --- /dev/null +++ b/spec/browser/options_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +describe Ferrum::Browser::Options do + describe "#protocol_timeout" do + it "defaults to a low value, sufficient for internal CDP bookkeeping" do + options = described_class.new + + expect(options.protocol_timeout).to eq(Ferrum::Browser::Options::DEFAULT_PROTOCOL_TIMEOUT) + end + + it "does not shrink when :timeout is lowered" do + options = described_class.new(timeout: 0.1) + + expect(options.timeout).to eq(0.1) + expect(options.protocol_timeout).to eq(Ferrum::Browser::Options::DEFAULT_PROTOCOL_TIMEOUT) + end + + it "is configurable independently of :timeout" do + options = described_class.new(timeout: 10, protocol_timeout: 60) + + expect(options.timeout).to eq(10) + expect(options.protocol_timeout).to eq(60) + end + end +end diff --git a/spec/page/screenshot_spec.rb b/spec/page/screenshot_spec.rb index a85c3e14..22465bc2 100644 --- a/spec/page/screenshot_spec.rb +++ b/spec/page/screenshot_spec.rb @@ -215,6 +215,7 @@ def create_screenshot(**options) allow(browser.page).to receive(:command).and_call_original expect(browser.page).to receive(:command) .with("Page.captureScreenshot", + timeout: Ferrum::Page::Screenshot::DEFAULT_RENDER_TIMEOUT, format: "png", clip: { x: 0, y: 0, width: 1280, height: 1024, scale: 1.0 }, captureBeyondViewport: true) .and_raise(StandardError) diff --git a/spec/unit/client_spec.rb b/spec/unit/client_spec.rb index 29f800f9..316b7f94 100644 --- a/spec/unit/client_spec.rb +++ b/spec/unit/client_spec.rb @@ -48,4 +48,55 @@ def +(other) end end end + + describe "#send_message" do + before do + client.instance_variable_set(:@pendings, Concurrent::Hash.new) + end + + it "waits up to protocol_timeout, independent of the page-level timeout" do + options = Ferrum::Browser::Options.new(timeout: 100, protocol_timeout: 0.05) + client.instance_variable_set(:@options, options) + client.instance_variable_set( + :@ws, instance_double(Ferrum::Client::WebSocket, send_message: true, messages: Queue.new) + ) + + expect do + client.send_message(client.build_message("Target.createTarget", {}), async: false) + end.to raise_error(Ferrum::TimeoutError) + end + + it "does not time out early just because the page-level timeout is low" do + options = Ferrum::Browser::Options.new(timeout: 0.05, protocol_timeout: 100) + client.instance_variable_set(:@options, options) + + # Simulates the response arriving after the (low) page-level timeout has + # already elapsed, but well inside the (high) protocol timeout. + ws = instance_double(Ferrum::Client::WebSocket, messages: Queue.new) + allow(ws).to receive(:send_message) do |message| + Thread.new do + sleep 0.1 + client.instance_variable_get(:@pendings)[message[:id]].set({ "result" => { "ok" => true } }) + end + true + end + client.instance_variable_set(:@ws, ws) + + message = client.build_message("Target.createTarget", {}) + expect(client.send_message(message, async: false)).to eq({ "ok" => true }) + end + + it "honors an explicit per-call timeout: over protocol_timeout" do + options = Ferrum::Browser::Options.new(protocol_timeout: 100) + client.instance_variable_set(:@options, options) + client.instance_variable_set( + :@ws, instance_double(Ferrum::Client::WebSocket, send_message: true, messages: Queue.new) + ) + + message = client.build_message("Page.navigate", {}) + expect do + client.send_message(message, async: false, timeout: 0.05) + end.to raise_error(Ferrum::TimeoutError) + end + end end From d538c783c387b919a7a2750be01c96b941a607c0 Mon Sep 17 00:00:00 2001 From: Dmitry Vorotilin Date: Thu, 20 Aug 2026 12:53:34 +0300 Subject: [PATCH 2/2] fix: tests --- CHANGELOG.md | 6 ++++-- lib/ferrum/browser.rb | 2 +- lib/ferrum/browser/options.rb | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2edb27ed..fbcb207a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,10 @@ ## [Unreleased](https://github.com/rubycdp/ferrum/compare/v0.17.2...main) ## ### Added -- `Ferrum::Browser` option `:protocol_timeout` (default 1s, independent of `:timeout`) bounds individual internal CDP - bookkeeping calls (`Target.createTarget`, `Target.attachToTarget`, etc. +- `Ferrum::Browser` option `:protocol_timeout` bounds individual internal CDP bookkeeping calls, e.g. + `Target.createTarget`, `Target.attachToTarget`. It's a separate setting from `:timeout`: its default (5s) happens + to match `:timeout`'s own default, but passing `timeout:` to `Browser.new` does not change it -- set + `:protocol_timeout` (or the `FERRUM_PROTOCOL_TIMEOUT` env var) explicitly for a different value. - `Ferrum::Page#pdf`/`#screenshot` accept a `timeout:` argument (default 60s) for the CDP call itself. Generating a full-page screenshot or a large PDF is a known slow outlier among CDP commands. - `Ferrum::Frame#loader_id` provides a loader id when the frame navigates [#583] diff --git a/lib/ferrum/browser.rb b/lib/ferrum/browser.rb index 6df3fb45..196f5d65 100644 --- a/lib/ferrum/browser.rb +++ b/lib/ferrum/browser.rb @@ -89,7 +89,7 @@ class Browser # The number of seconds we'll wait for a response when communicating # with the browser: navigations, JS evaluation, DOM queries, dispatching input, etc. # - # @option options [Numeric] :protocol_timeout (1) + # @option options [Numeric] :protocol_timeout (5) # The number of seconds we'll wait for an individual internal CDP # bookkeeping call to respond, e.g. `Target.createTarget`, # `Target.attachToTarget`. These normally resolve in milliseconds. diff --git a/lib/ferrum/browser/options.rb b/lib/ferrum/browser/options.rb index 6b9fe13c..0a6cbec0 100644 --- a/lib/ferrum/browser/options.rb +++ b/lib/ferrum/browser/options.rb @@ -14,7 +14,7 @@ class Options WINDOW_SIZE = [1024, 768].freeze BASE_URL_SCHEMA = %w[http https].freeze DEFAULT_TIMEOUT = ENV.fetch("FERRUM_DEFAULT_TIMEOUT", 5).to_i - DEFAULT_PROTOCOL_TIMEOUT = ENV.fetch("FERRUM_PROTOCOL_TIMEOUT", 1).to_i + DEFAULT_PROTOCOL_TIMEOUT = ENV.fetch("FERRUM_PROTOCOL_TIMEOUT", DEFAULT_TIMEOUT).to_i PROCESS_TIMEOUT = ENV.fetch("FERRUM_PROCESS_TIMEOUT", 10).to_i DEBUG_MODE = !ENV.fetch("FERRUM_DEBUG", nil).nil?