Firefox WebDriver BiDi Implementation for Test Frameworks

This article provides an overview of how the WebDriver BiDi (Bidirectional) automation protocol is architected and implemented within the Mozilla Firefox browser. It explains how Firefox handles low-latency, two-way communication using its internal Remote Agent, routes commands across a multi-process environment via the MessageHandler framework, hooks into native Gecko APIs, and ultimately enables modern testing frameworks like Puppeteer and Playwright to automate the browser without relying on proprietary protocols.

The Role of the Firefox Remote Agent

At the core of Firefox's WebDriver BiDi implementation is an internal component called the Remote Agent. Historically, browser automation via WebDriver Classic relied on an external binary (Geckodriver) translating HTTP requests into Firefox's internal Marionette protocol. WebDriver BiDi requires a fast, persistent, two-way connection so the browser can push events (like console logs or network requests) to the testing script the moment they happen.

When Firefox is launched in automation mode, the Remote Agent spins up an embedded HTTP server directly inside the browser. When a testing framework initiates a BiDi session, the Remote Agent upgrades the HTTP connection to a WebSocket connection. This WebSocket serves as the bidirectional pipeline for receiving JSON-RPC commands from the test runner and emitting asynchronous browser events back to it.

Multi-Process Routing via the MessageHandler

Modern Firefox uses a multi-process architecture (known as Project Fission) to isolate web content for security and stability. A single browser session consists of a main Parent Process and multiple Content Processes for different tabs and cross-origin iframes.

To execute WebDriver BiDi commands across these isolated processes, Firefox engineers built the MessageHandler framework:

  1. The Root Scope: The WebSocket connection terminates in the Parent Process. A Root MessageHandler receives the incoming JSON-RPC payload, validates it, and determines where the command needs to be executed.
  2. Inter-Process Communication (IPC): If a command handles browser-wide tasks (like intercepting network traffic or creating a new tab), it stays in the Parent Process. If it interacts with a webpage (like evaluating JavaScript or extracting DOM elements), the Root MessageHandler forwards the command to the appropriate Content Process using Firefox’s JSWindowActor IPC mechanism.
  3. The WindowGlobal Scope: Inside the Content Process, a WindowGlobal MessageHandler receives the forwarded command, executes it against the specific frame or document, and sends the result back up to the Parent Process, which relays it over the WebSocket.

This architecture also works in reverse for events. When a test script subscribes to an event (such as log.entryAdded), the Root MessageHandler broadcasts this subscription to all existing and future Content Processes. When a webpage logs to the console, the Content Process captures it and pushes the data up to the Parent Process to be sent to the testing framework.

Mapping BiDi Modules to Gecko Internals

The WebDriver BiDi specification is divided into distinct modules, such as session, browsingContext, script, network, and log. In Firefox, each module is implemented as an isolated JavaScript class within the MessageHandler ecosystem that interfaces directly with Gecko (Firefox's layout engine) and SpiderMonkey (Firefox's JavaScript engine).

  • Network Module: Implemented in the Parent Process, this module hooks directly into Necko, Firefox's networking library. It observes HTTP channels to report request and response data, and allows test frameworks to intercept, modify, or fail network requests on the fly.
  • Script and Log Modules: Implemented primarily in the Content Process, these modules utilize the Debugger API provided by SpiderMonkey. This is the same underlying API used by the Firefox DevTools. It allows the BiDi implementation to evaluate scripts in specific sandboxes, manage object references (handles), and listen for uncaught exceptions and console API calls.
  • BrowsingContext Module: This module spans both Parent and Content processes to manage tabs, windows, and iframes. It listens to internal Gecko window management and navigation events to accurately report when a page has started loading, when the DOMContentLoaded event fires, and when navigation is complete.

Integration with Puppeteer and Playwright

Testing frameworks like Puppeteer and Playwright require low-level control and event-driven architecture to prevent flaky tests. Previously, achieving this in Firefox required workarounds: Puppeteer relied on an experimental, incomplete implementation of the Chrome DevTools Protocol (CDP) built into Firefox, while Playwright maintained a heavily patched, custom build of Firefox using a proprietary juggler protocol.

WebDriver BiDi unifies this landscape by offering a standardized protocol natively inside stock Firefox:

  • Puppeteer: Puppeteer now uses WebDriver BiDi as its default way to automate Firefox. When Puppeteer launches Firefox, it connects to the Remote Agent's WebSocket endpoint. Puppeteer’s internal BiDi adapter translates high-level Puppeteer API calls (like page.goto() or page.evaluate()) directly into standard WebDriver BiDi JSON-RPC payloads.
  • Playwright: The implementation of BiDi in Firefox provides the exact primitives Playwright needs—such as execution context tracking, network interception, and realm isolation—natively in the browser. This allows Playwright to transition toward using standard Firefox releases via WebDriver BiDi rather than maintaining custom browser forks.

Hybrid Execution with Geckodriver

While Puppeteer and Playwright communicate exclusively over WebSockets, other tools (like Selenium) use a mix of traditional HTTP WebDriver commands and modern BiDi commands. Firefox supports this hybrid model through Geckodriver. When a session is started via Geckodriver with the "webSocketUrl": true capability, Geckodriver launches Firefox, negotiates the connection with the internal Remote Agent, and acts as a proxy. This allows test runners to use legacy commands alongside real-time BiDi event listeners in the exact same browsing session.