Caitlin Rainone Caitlin Rainone - 6 months ago 39
Javascript Question

Why doesn't Firefox allow you to execute a script in the main frame?

I'm writing a webextension using Firefox's new webExtensions framework, which is based on Chrome's. All the sample extensions work in the Nightly build, so that's where I'm testing. What I'm trying to do is run a script on the content page when it loads. Here's my background.js page:

(edited with corrections from Rob W)

"use strict";

function onCompletedFunc(details) {
var script = 'console.log("ok");';
console.log("Details are %o", details);
if(details.frameId == 0) {
chrome.tabs.executeScript(details['tabId'], {
code: script,
runAt: 'document_end'

//Works on subrequests, but not the main frame:
{'urls': ['<all_urls>']},
//Does not work:
{'urls': ['<all_urls>'], 'types':["main_frame"]},

//@Rob W, works:

//Really should work, but doesn't when the tab's status is still "loading"
{'urls': ['<all_urls>'], 'types':["main_frame"]},
function onCompletedFunc2(details) {
var script = 'console.log("ok");';
console.log("Starting tab check, details are %o", details);
chrome.tabs.query({}, function(tablist) {
for(var tab in tablist) {
if(tablist[tab].id == details.tabId) {
console.log("now injecting to %o", tablist[tab]);
chrome.tabs.executeScript(tablist[tab].id, {'code': script, 'runAt': 'document_end'});

This works in Nightly. I get a list of details ("Details are ...") from the background page and a few lines of "ok" on the console on the content page, one for each resource it loaded. It runs the same in Chrome (the manifest file is slightly different).

What I want is the second variation, which only runs once on the page. This works fine in Chrome. In Nightly, it shows the one "Details" message from the main frame, but nothing shows up in the console on the content page.

I'm sure this is a timing problem, but what's wrong here?

manifest.json (remove "applications":{...} for use with Chrome)


"description": "",
"manifest_version": 2,
"name": "execute_script",
"version": "1.0",

"applications": {
"gecko": {
"id": "",
"strict_min_version": "45.0"

"permissions": [
"webRequest", "webRequestBlocking", "webNavigation", "<all_urls>"

"background": {
"scripts": ["background.js"]



Your first snippet (without types) only works because you're not just getting requests for main_frame, but also another subresource. webRequest.onCompleted is not the right event if you want to inject a script. There is no guarantee that the page in the tab matches with the one for which you've received a request.

If you want to unconditionally run code, just declare the content script in the manifest file. For more info, see the content script documentation.

If programmatic injection is a must, use the chrome.webNavigation.onCommitted event instead of the webRequest event. With this event, you know that the tab is now showing the response to the given URL. For example:

chrome.webNavigation.onCommitted.addListener(function(details) {
    // Example: Only run in main frame and URLs containing "example"
    if (details.frameId === 0 && details.url.includes('example')) {
        chrome.tabs.executeScript(details.tabId, {
            code: 'console.log("Injected at " + document.URL);',
            runAt: 'document_end'
// filter (the second parameter, not used above) is supported as of Firefox 50.

Event if you use webNavigation.onCommitted, you have to take into account that the APIs are non-blocking. It is possible (but with a low probability) that the following happens:

  1. User starts request.
  2. webNavigation.onCompleted fires.
  3. You invoke tabs.executeScript.
  4. Meanwhile the tab navigates to some other site.
  5. The tabs.executeScript request arrives in the tab, which has meanwhile switched to a different URL.
  6. Oops, you have just injected a script in a page that differs from the one that you intended.

If the URL mismatch is not desirable, then check document.URL or location has the expected value before continuing with the content script logic.