Skip to content

fix(browser): let quit clean up a browser whose contexts were never built - #634

Merged
route merged 1 commit into
rubycdp:mainfrom
PhilMeyr:fix/quit-nil-contexts
Sep 8, 2026
Merged

fix(browser): let quit clean up a browser whose contexts were never built#634
route merged 1 commit into
rubycdp:mainfrom
PhilMeyr:fix/quit-nil-contexts

Conversation

@PhilMeyr

@PhilMeyr PhilMeyr commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Problem

Browser#quit guards on @client but then calls contexts.close_connections unconditionally:

def quit(wait: true)
  return unless @client

  contexts.close_connections

  @client.close
  thread = @process.stop(wait: wait)
  @client = @process = @contexts = nil
  thread
end

start can leave the browser with exactly the state that guard doesn't cover — @client set and @contexts still nil:

@client = Client.new(@process.ws_url, options)
@contexts = Contexts.new(@client)
rescue StandardError
  @process.stop
  raise
end

Contexts.new sends CDP commands (find_implicit_context, subscribe, auto_attach, discover), so a browser that dies right after the websocket handshake raises there, after @client was assigned. The rescue stops the process and re-raises without clearing the ivars.

That state is unrecoverable. restart is quit; start, so the next restart enters quit, passes return unless @client, and raises NoMethodError: undefined method 'close_connections' for nil before reaching the line that resets the ivars. Every subsequent restart fails the same way, on the same object, forever.

We hit this in production: Chrome started, aborted a moment later, and the long-lived browser singleton in our PDF worker was wedged until the process was restarted by hand. Downstream code that reasonably checks browser.client before restarting sees a non-nil client and never even attempts recovery.

Fix

Safe-navigate the contexts call. quit then does the right thing for a half-started browser: @client.close releases the websocket and its thread, @process.stop reaps the process, and the ivars are cleared, so the following start runs on a clean object.

This is deliberately the smaller of the two possible fixes. Clearing @client/@contexts in start's rescue instead would leave the client unclosed — letting quit handle it keeps the existing cleanup path responsible for cleanup.

The adjacent page&.close / context&.dispose in create_page guard the same way, for the same reason (#582).

Regression spec

Added to #restart: stub Contexts.new to raise, let one restart fail, un-stub, and assert the browser restarts. Without the fix it fails with

expected no Exception, got #<NoMethodError: undefined method 'close_connections' for nil>
  ./lib/ferrum/browser.rb:252:in 'Ferrum::Browser#quit'
  ./lib/ferrum/browser.rb:234:in 'Ferrum::Browser#restart'

bundle exec rspec spec/browser_spec.rb — 52 examples, 0 failures, 3 pending. Rubocop clean.

@route

route commented Sep 4, 2026

Copy link
Copy Markdown
Member

I think we need to use attr_reader vs ivar and browser_spec is for systems spec, not unit tests

…uilt

When start raises after Client.new but before Contexts.new returns, the
browser keeps a non-nil @client and a nil @contexts. quit passes its
@client guard, then calls contexts.close_connections and dies with
NoMethodError before resetting any state, so restart can never recover.

Guarding with safe navigation lets quit close the client, stop the
process and clear the ivars, which is exactly what restart needs.
@PhilMeyr
PhilMeyr force-pushed the fix/quit-nil-contexts branch from d6f3c2d to be314ad Compare September 7, 2026 14:39
@PhilMeyr

PhilMeyr commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Both done, thanks.

contexts&.close_connections — the reader was there before my patch and switching to the ivar was a gratuitous inconsistency on my part.

The regression spec moved to spec/unit/browser_spec.rb, and now raises a plain StandardError to match the surrounding style.

@route

route commented Sep 8, 2026

Copy link
Copy Markdown
Member

thanks!

@route
route merged commit ea93308 into rubycdp:main Sep 8, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants