Fix moz-extension:// navigation hang by bypassing geckodriver#87
Conversation
|
Verified locally on a worktree of this PR. The bypass is legit: skips geckodriver entirely and goes straight over the BiDi WebSocket via FirefoxCore.sendBiDiCommand with wait: 'none'.... |
There was a problem hiding this comment.
Heads up — the Format check step on CI is red:
https://github.com/mozilla/firefox-devtools-mcp/actions/runs/25980005541/job/76504149200?pr=87
Prettier flags two of the new test files:
tests/firefox/pages.test.ts
tests/integration/moz-extension.integration.test.ts
Should be a one-shot npm run format and re-push.
|
@freema please hold off merging this, I am checking whether navigating to moz-extension is something we can allow here. We are currently restricting navigation to non-http/https pages unless you request privileged access, so we should be careful with workarounds here. |
| if (isPrivilegedUrl(url) && this.sendBiDiCommand) { | ||
| const contextId = this.getCurrentContextId(); | ||
| if (!contextId) { | ||
| throw new Error(`Cannot navigate to privileged URL ${url}: no browsing context ID`); | ||
| } | ||
| await this.sendBiDiCommand('browsingContext.navigate', { | ||
| context: contextId, | ||
| url, | ||
| wait: 'none', | ||
| }); | ||
| logDebug(`BiDi navigate (wait:none) to: ${url}`); | ||
| } else { | ||
| await this.driver.get(url); | ||
| } |
There was a problem hiding this comment.
Instead of switching between BiDi and Classic, I would rather suggest using BiDi in all cases, and simply specifying wait=none when the URL is privileged
|
Navigation to non http/https is likely to be restricted on the Firefox side to only work with sessions are started with --remote-allow-system-access, which maps to the @csamu can you describe your scenario here? I suspect we should allow this only when |
It's useful during extension development with agents.
So fetching the UUIDs is already gated behind Before this fix, I used a trick to test extensions through the MCP, even without
Regardless, if a restriction should be added, a proposal would be to put |
|
Alright, thanks for the feedback, supporting webextension development makes a lot of sense. That's not an area where webdriver classic / bidi offers a lot, compared to the actual Firefox DevTools. In the long run, maybe it would make sense to have tools dedicated to webextensions so that you don't have to get the UUID and build the URL. For now it sounds fine to add a way to allow the navigation when I think for now it's fine to modify the navigate tool to automatically switch to wait=none when the scheme is not http/https/data/blob/file. Firefox can be responsible for now to fail the navigation or not (and once the implementation has settled on the Firefox side, we can update the MCP to have a dedicated command for "privileged" protocols). Can you update the PR to:
Thanks! |
Replace isPrivilegedUrl with isCommonScheme (negation check). Navigate always uses BiDi browsingContext.navigate: common schemes (http/https/data/blob/file) get wait:interactive, uncommon schemes (moz-extension:/about:/etc.) get wait:none. sendBiDiCommand is now required. Trim unit tests. Add afterEach reset and tab cleanup to integration tests.
juliandescottes
left a comment
There was a problem hiding this comment.
Thanks for the update! That looks good to me, I would just simplify a bit the test and remove raceWithTimeout. I will merge a bit later, if you have time to update it before that great, if not I can follow up.
| throw new Error(`Cannot navigate: no browsing context ID`); | ||
| } | ||
|
|
||
| // Default wait time is "interactive" (DOMContentLoaded). |
There was a problem hiding this comment.
To be completely transparent here, maybe complete would be a more faithful translation of the current implementation. However BiDi is much more strict than Classic when it comes to navigation, so I prefer to stick with interactive for now.
| // Non-standard schemes use wait:"none" because the Remote Agent | ||
| // doesn't emit navigation completion events for extension contexts. | ||
| // A 5s timeout proves no hang while tolerating slow CI. | ||
| const result = await raceWithTimeout(firefox.navigate(extensionUrl), 5000); |
There was a problem hiding this comment.
I don't think the raceWithTimeout is necessary here. The test timeout is set to 15s so it's not a significant difference compared to 5s.
I would remove the two call sites and the helper.
|
Thanks for the update, all tests seem to be passing, I'm going to merge this. |
The problem
TLDR: Navigating to
moz-extension://URLs doesn't work.driver.get()hangs because the underlying BiDi command never gets a response.When navigating to a
moz-extension://URL, thePageManagement.navigate()method uses Selenium to send a HTTP request to Geckodriver that sends a WebDriver BiDi request to Firefox (the BiDi Remote Agent server) to load the extension page. The Remote Agent recives the message and Firefox changes the page. The problem is that Firefox never triggers the "i am done" event when loading extension pages, thus never triggers the Remote Agent to send a response back to Geckodriver, leaving Geckodriver listening for a response indefinitely. There is no timeout.This is the same class of bug as the zombie-geckodriver fix: geckodriver blocks indefinitely when Firefox doesn't respond. In the zombie case, Firefox is gone entirely. Here, Firefox is alive but the Remote Agent never responds.
So there are actually two upstream bugs, one in Geckodriver and one in the Remote Agent.
This fix is just a workaround that makes the MCP work anyway. When the actual bugs are fixed, this becomes unecessary.
The solution
TLDR: Bypass Geckodriver entirely and send a manual BiDi request over WebSocket to Firefox.
Instead of using
driver.get()to query Firefox, which uses Geckodriver, we instead send a manual WebDriver BiDi commandbrowsingContext.navigatewithwait: "none"usingFirefoxCore.sendBiDiCommand, causing the Remote Agent to load the page and respond immediately.The downside is that there is no timing guarantee. The extension might not actually have finished loading when the MCP tool call returns. But in an AI agent context, it likely doesn't matter in practice
Files changed
src/firefox/index.ts- PasssendBiDiCommandcallback toPageManagementsrc/firefox/pages.ts- AddisPrivilegedUrl(). Add BiDi navigation tonavigate(). Usenavigate()increateNewPage()Tests
tests/firefox/pages.test.ts- Unit tests for isPrivilegedUrl, navigate and the createNewPage delegationtests/integration/moz-extension.integration.test.ts- Integration tests with real Firefox: installs a fixture extension, resolves its UUID to construct valid moz-extension:// URLs, and verifies navigation without hangingtests/fixtures/test-extension/- Minimal extension fixture for integration tests