Skip to content

Potential regression in implicit browser context handling with flatten: false and Browserless #578

@CamilleDrapier

Description

@CamilleDrapier

Describe the bug

After updating ferrum from 0.17.1 to 0.17.2, it seems that all my system specs are failing with: Failed to find browser context for id XXX.

To Reproduce

1. Start Browserless (assuming docker)

docker run --rm -p 3311:3311 -e PORT=3311 -e CONNECTION_TIMEOUT=600000 browserless/chrome:latest

1. Use Browserless to create a page

require 'ferrum'
browser = Ferrum::Browser.new(url: "http://localhost:3311", flatten: false)
browser.create_page

You should see: Failed to find browser context with id XXX (Ferrum::BrowserError)

Expected behavior

I should be able to create a page

Desktop:

  • OS: Linux
  • Browser Chromium 146.0.7680.177 (Official Build) Arch Linux (64-bit)]
  • Version 0.17.2

Additional context

Here is an analysis of the problem by Cursor/Opus 🤖 :

Details

PR #566 (v0.17.2) introduced two changes that together break any setup using flatten: false with a remote Chrome/Browserless:

  1. auto_attach lost its flatten guard. In v0.17.1, auto_attach was a no-op when flatten: false:

    # v0.17.1
    def auto_attach
      return unless @client.options.flatten  # ← skipped when flatten: false
      @client.command("Target.setAutoAttach", autoAttach: true, waitForDebuggerOnStart: true, flatten: true)
    end

    In v0.17.2 (fix: browser.reset tries to dispose default implicit context #540 #566), the guard was removed:

    # v0.17.2
    def auto_attach
      @client.command("Target.setAutoAttach", autoAttach: true, waitForDebuggerOnStart: true, flatten: true)
    end

    Now auto_attach always fires Target.setAutoAttach, which triggers Target.attachedToTarget CDP events for existing targets. These events call add_context, which captures the browser's implicit default context and promotes it to @default_context via @default_context ||= context. This implicit context is not usable for Target.createTarget.

  2. reset conditionally clears @default_context. In v0.17.1, reset unconditionally set @default_context = nil. In v0.17.2, it only clears it if Target.getBrowserContexts lists it — but that API never returns implicit contexts. So after reset, @default_context still points to the stale implicit context.

The combination means:

  • First test fails@default_context is the implicit context; create_target is rejected by Chrome.
  • All subsequent tests failreset never clears @default_context because the implicit context isn't in the getBrowserContexts list.

Why flatten: false matters

flatten: false is the documented workaround for using Ferrum/Cuprite with Browserless (see #540 comments). In v0.17.1, flatten: false also implicitly prevented auto_attach from running, which kept the implicit context out of Ferrum's state. The v0.17.2 removal of the guard broke this.

Root cause: the three interacting changes

1. auto_attach — guard removed (primary cause)

 def auto_attach
-  return unless @client.options.flatten
-
   @client.command("Target.setAutoAttach", autoAttach: true, waitForDebuggerOnStart: true, flatten: true)
 end

With flatten: false, this was previously a no-op. Now it always runs, causing Target.attachedToTarget events that feed add_context.

2. add_context — promotes implicit context to @default_context

def add_context(context_id)
  return if @contexts[context_id]

  context = Context.new(@client, self, context_id)
  @contexts[context_id] = context
  @default_context ||= context   # ← implicit context becomes default
end

This preempts default_context's @default_context ||= create, so a proper explicit context is never created.

3. reset — conditional clear (secondary cause)

 def reset
-  @default_context = nil
-  @contexts.each_key { |id| dispose(id) }
+  context_ids = @client.command("Target.getBrowserContexts")["browserContextIds"]
+  @default_context = nil if context_ids.include?(@default_context&.id)
+  @contexts.each_key { |id| dispose(id) if context_ids.include?(id) }
 end

Target.getBrowserContexts only returns explicitly created contexts. The implicit context is never in the list, so @default_context is never cleared between tests.

To reproduce

1. Start Browserless

docker run --rm -p 3311:3311 -e PORT=3311 -e CONNECTION_TIMEOUT=600000 browserless/chrome:latest

Wait until the service is ready, then verify:

curl -s http://localhost:3311/json/version

2. Minimal reproduction script

Create repro.rb (only dependency: gem install ferrum -v 0.17.2):

require 'ferrum'

# flatten: false is the documented workaround for Browserless (#540).
# This worked in 0.17.1 but breaks in 0.17.2.
browser = Ferrum::Browser.new(url: "http://localhost:3311", flatten: false)

# Inspect internal state
puts "Contexts: #{browser.contexts.contexts.keys}"
puts "@default_context: #{browser.contexts.instance_variable_get(:@default_context)&.id}"

# Attempt 1: fails because @default_context is the implicit context
begin
  browser.create_page
  puts "✅ create_page succeeded"
rescue Ferrum::BrowserError => e
  puts "❌ create_page FAILED: #{e.message}"
end

# reset doesn't help — conditional clear doesn't apply to implicit contexts
browser.reset
puts "@default_context after reset: #{browser.contexts.instance_variable_get(:@default_context)&.id}"

# Attempt 2: still fails
begin
  browser.create_page
  puts "✅ create_page after reset succeeded"
rescue Ferrum::BrowserError => e
  puts "❌ create_page after reset FAILED: #{e.message}"
end

browser.quit

With Ferrum 0.17.1 — passes (auto_attach is skipped, no implicit context captured):

Contexts: []
@default_context:
✅ create_page succeeded

With Ferrum 0.17.2 — fails:

Contexts: ["<implicit-context-id>"]
@default_context: <implicit-context-id>
❌ create_page FAILED: Failed to find browser context for id <implicit-context-id>
@default_context after reset: <implicit-context-id>
❌ create_page after reset FAILED: Failed to find browser context for id <implicit-context-id>

3. Cuprite/Capybara (real-world scenario)

With Cuprite using the standard flatten: false workaround for Browserless, all system specs fail:

Capybara.register_driver(:cuprite) do |app|
  Capybara::Cuprite::Driver.new(app, {
    url: ENV.fetch('CHROME_URL'),
    flatten: false,              # Required for Browserless compatibility
    window_size: [1200, 800],
    browser_options: { 'no-sandbox' => nil }
  })
end
Ferrum::BrowserError:
  Failed to find browser context for id 837C66659427FDF757025C7FD799AE4A

Expected behavior

With flatten: false and a remote Browserless instance, default_context should return a usable, explicitly created browser context — same as in v0.17.1.

Proposed fix

Option A: Restore the flatten guard in auto_attach

The simplest fix — if flatten: false, don't send Target.setAutoAttach:

def auto_attach
  return unless @client.options.flatten

  @client.command("Target.setAutoAttach", autoAttach: true, waitForDebuggerOnStart: true, flatten: true)
end

This restores v0.17.1 behavior. If the guard was removed intentionally (e.g., to fix another issue), see Option B.

Option B: Don't promote implicit contexts + always clear on reset

If auto_attach must always run, then prevent implicit contexts from becoming @default_context:

def add_context(context_id)
  return if @contexts[context_id]

  context = Context.new(@client, self, context_id)
  @contexts[context_id] = context
  # Removed: @default_context ||= context
end

def reset
  context_ids = @client.command("Target.getBrowserContexts")["browserContextIds"]
  @default_context = nil    # Always clear — implicit contexts aren't in getBrowserContexts
  @contexts.each_key { |id| dispose(id) if context_ids.include?(id) }
end

This preserves the #566 fix (not disposing implicit contexts) while ensuring default_context always calls create for a usable context.

Workaround

Until fixed upstream, prepend a patch in test setup (we use Option B):

module FerrumContextsResetPatch
  def reset
    super
    @default_context = nil
  end

  private

  def add_context(context_id)
    return if contexts[context_id]

    context = Ferrum::Context.new(@client, self, context_id)
    contexts[context_id] = context
  end
end

Ferrum::Contexts.prepend(FerrumContextsResetPatch)

Environment

  • Ferrum: 0.17.2 (regression from 0.17.1)
  • Cuprite: 0.17
  • Chrome/Browserless: browserless/chrome:latest
  • Ruby: 3.4.x
  • OS: Linux (Ubuntu 24.04)

Related

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions