onMessage.addListener doesn't always receive messages from sendMessage in google chrome extension

301 views
Skip to first unread message

Y M.

unread,
May 7, 2024, 10:02:38 AMMay 7
to Chromium Extensions
I communicate between my content script and my background script using chrome.runtime.onMessage.addListener to receive the message on the front in my content.js, and I send messages using chrome.tabs.sendMessage in the background.js script.

On the content.js script, at the start of the file, I have this:

    chrome.runtime.onMessage.addListener((msg, sender) => {
      if (msg.command !== 'log') {
        console.log(msg);
      }
   
      // First, validate the message's structure.
      if (msg.command === 'Browse Startups') {
        // reset the list of startups browsed
        chrome.storage.local.set({'done': []}, function() {
          console.log('Startups Reset');
          chrome.runtime.sendMessage({'action': 'reset'});
        });
        browseStartups();
        return true;
      } else if ((msg.command === 'Check Startup')) {
        shouldStartupBeDone(msg.startup);
        return true;
      } else if ((msg.command === 'Do Startup')) {
        doStartupProfile(msg.url, msg.key, msg.total);
        return true;
      } else if ((msg.command === 'Do Teammate')) {
        doTeammateProfile(msg.startup, msg.teammate);
        return true;
      } else if ((msg.command === 'Save Startup')) {
        saveStartupProfile(msg.startup);
        return true;
      } else {
        console.log('ERROR: this command does not exist');
        return true;
      }

    });

and on the background script I do this:

    function startDoing(startup, tabId, key, total) {
        console.log('startDoing');
        chrome.tabs.sendMessage(tabId, { command: 'Do Startup', url: startup.url, key: key, total: total });
    }


this function is called after the page finished loading like this:

    chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
        if (changeInfo.status == 'complete' && tab.active) {
            chrome.storage.local.get(['nextAction', 'startups', 'key', 'teammate', 'startup'], (request) => {
                console.log(request);
   
                chrome.storage.local.set({ nextAction: null }, () => {
                    if (request.startups !== undefined && request.nextAction !== null) {
                        console.log('start');
                        setTimeout(() => {
                            startDoing(request.startups[request.key], tabId, request.key, request.startups.length);
                        }, 7000);
                    }
                });
            });
        }
    });


From the logs, I can see that when the page is loaded, my background script sends the message to the front, but the front does not receive it and nothing happens, the DoStartup function does not work and my script then breaks.

It does not happen all the time, maybe 1% of the time, but it's enough to break everything.

Do you know why it happens? Is it the best way to launch a specific content.js script after the page finishes loading?

Here is my manifest:

    {
        "manifest_version": 3,
        "name": "My App",
        "short_name": "My App",
        "description": "My App",
        "version": "1.0",
        "action": {
            "default_icon": "icon.png",
            "default_title": "My App",
            "default_popup": "popup/popup.html"
        },
        "icons": {
            "16": "icon.png",
            "48": "icon.png",
            "128": "icon.png"
        },
        "permissions": [
            "tabs",
            "activeTab",
            "cookies",
            "storage",
            "webNavigation"
        ],
        "background": {
            "service_worker": "background/background.js"
        },
        "content_scripts": [{
            "matches": [
                "http://*/*",
                "https://*/*"
            ],
            "js": [
                "vendors/jquery.min.js",
                "content/content.js"
            ]
        }]
    }

wOxxOm

unread,
May 7, 2024, 10:20:39 AMMay 7
to Chromium Extensions, Y M.
You need to re-inject your declared content_scripts after reloading/installing the extension in Chrome: https://stackoverflow.com/a/11598753

However, if your extension needs to interact only with the active tab, it's best not to inject into all tabs unnecessarily as it wastes CPU and memory.
* remove content_scripts
* add "scripting" to "permissions"
* add "host_permissions": ["<all_urls>"]
* inject on demand using chrome.scripting.executeScript

function startDoing(startup, tabId, key, total) {
  send(tabId, { command: 'Do Startup', url: startup.url, key: key, total: total });
}

async function send(tabId, msg) {
  for (let i = 0; i < 2; i++) {
    try {
      return await chrome.tabs.sendMessage(tabId, msg);
    } catch (err) {
      await chrome.scripting.executeScript({
        target: { tabId },
        files: ['vendors/jquery.min.js', 'content/content.js'],
      });
    }
  }
}

Y M.

unread,
May 7, 2024, 12:14:27 PMMay 7
to Chromium Extensions, wOxxOm, Y M.
Thank you so much.

Should I put the bit of code that injects the code provided in the link you sent me in my chrome.tabs.onUpdated.addListener function or should I add the chrome.runtime.onInstalled.addListener like they did in the example?

Also in the for loop, should I compare the tabId somewhere to prevent the scripts from being added to every tabs as you suggested?

Thanks



wOxxOm

unread,
May 7, 2024, 2:30:21 PMMay 7
to Chromium Extensions, Y M., wOxxOm
One solution is chrome.runtime.onInstalled as described in the linked article. Another one in the comment itself is a more efficient alternative to be used in onUpdated or any other place. 

> should I compare the tabId somewhere to prevent the scripts from being added to every tabs as you suggested

It depends on what your extension does. The code you've shown so far says it wants to process any tab that's currently focused.

Y M.

unread,
May 9, 2024, 3:30:10 PMMay 9
to Chromium Extensions, wOxxOm, Y M.
Thank you for our continued support.

I have implemented the changes you suggested. It's very weird I still have the same issues.

After doing some more tests, I created a function from the content script that sends a message to the background script to say it's has received the request.

While I don't see anything happening on the console log of the browser, I can see that the background scripts is receiving the confirmation message sent from a content script, but nothing is happening in my tab and the console is not showing anything ... !

So it seems like the background script is communicating with the wrong tab or window, despite having only one tab and one window opened. The tab & console don't do/show anything, but the background script is receiving a confirmation that the content script did receive the message.

I'm very confused, do you know how is this happening?

Many thanks

wOxxOm

unread,
May 9, 2024, 3:32:58 PMMay 9
to Chromium Extensions, Y M., wOxxOm
Without seeing your new code I can only guess you still use content_scripts declaration and your script occasionally runs in a pre-rendered document, see https://developer.chrome.com/blog/extension-instantnav

Y M.

unread,
May 10, 2024, 6:37:24 AMMay 10
to Chromium Extensions, wOxxOm, Y M.
To provide more context, this is what I have in the background script:

let timeout = null;
chrome.runtime.onInstalled.addListener(async () => {
    for (const cs of chrome.runtime.getManifest().content_scripts) {
        for (const tab of await chrome.tabs.query({url: cs.matches})) {
          if (tab.url.match(/(chrome|chrome-extension):\/\//gi)) {
            continue;
          }
          chrome.scripting.executeScript({
            files: cs.js,
            target: {tabId: tab.id, allFrames: cs.all_frames},
            injectImmediately: cs.run_at === 'document_start',
          });
        }
    }
});


chrome.tabs.onUpdated.addListener(function(tabId, changeInfo, tab) {
        if (changeInfo.status == 'complete' && tab.active) {
            chrome.storage.local.get(['nextAction', 'startups', 'key', 'teammate', 'startup'], (request) => {
                console.log(request);
   
                chrome.storage.local.set({ nextAction: null }, () => {
                    if (request.startups !== undefined && request.nextAction !== null) {
                        console.log('start');
                        setTimeout(() => {
                            startDoing(request.startups[request.key], tabId, request.key, request.startups.length);
                        }, 7000);
                    }
                });
            });
        }
    });


 function startDoing(startup, tabId, key, total) {
        console.log('startDoing');
        chrome.tabs.sendMessage(tabId, { command: 'Do Startup', url: startup.url, key: key, total: total });
    }


chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
    triggerAction(msg, sender.tab.id);
});

and on the content script:
function doStartupProfile(url, key, total) {
  console.log('doStartupProfile');
  //contact hte background script to test
  chrome.runtime.sendMessage({'action': 'test'});
  // rest of the script
}

As you can see, I'm sending a message to the background script once my doStartupProfile is triggered, just to see if it received it. From the console of the browser, i don't see anything, and the 'dostartupProfile' is not displayed (and the rest of the script is not triggered), but I received the 'test' action in the listener, that triggered the triggerAction action (it logged the test action in the background script in the service worker)

I don't understand this behavior, how is it possible that my background script received that test action, while the front did not do anything? It's probably interacting with a different tab ... but I have not other tab active. DO you know where this can be coming from?

Many thanks

wOxxOm

unread,
May 10, 2024, 6:43:39 AMMay 10
to Chromium Extensions, Y M., wOxxOm
Maybe you have a filter in devtools console that hides the messages.

Anyway, this is something that should be debugged. Use the built-in debugger in devtools by placing breakpoints in the onMessage both in the content script and in the background script, then see what happens when it triggers.

Is there a reason you don't want on-demand injection as suggested in my first comment?

wOxxOm

unread,
May 10, 2024, 6:59:28 AMMay 10
to Chromium Extensions, wOxxOm, Y M.
Another explanation may be that the message was sent by an old document in the tab, then the tab navigated to a new URL or it was reloaded, so the console was cleared (assuming you didn't enable "Preserve log" in console settings). If your content_scripts uses all_frames:true it could have been an iframe.

Also, either remove all "return true" or switch to one-time messaging via sendResponse callback parameter as shown in the documentation. Currently your code unnecessarily keeps the port open.

Y M.

unread,
May 10, 2024, 7:08:43 AMMay 10
to Chromium Extensions, wOxxOm, Y M.
I thought I did it by adding the chrome.runtime.onInstalled.addListener ?
If I have to add something else I will look into it.

I do not have a filter in the devtools, I triple checked that.

regarding your second message I will look into it, I'm a newbie in chrome dev so my code might not be optimal. But i also think it's interacting with another content.js that is not the one of the current tab. However, I am surprised it does it because this script is triggered by chrome.tabs.onUpdated.addListener and I wait for 7000ms with my timeout to make sure the page finished loading and I'm interacting with the right page.

Is there a way to make sure I"m interacting with the right content.js? I checked the tabId and it never change, so I don't think it's communicating with the wring tab.

Thanks

wOxxOm

unread,
May 10, 2024, 7:13:32 AMMay 10
to Chromium Extensions, Y M., wOxxOm
The tabId stays the same after navigation to a different page or reloading the tab. To ensure you send to the original document you should take docId=sender.documentId from onMessage listener and use it like this: chrome.tabs.sendMessage(tabId, msg, {documentId: docId})

Y M.

unread,
May 10, 2024, 12:56:53 PMMay 10
to Chromium Extensions, wOxxOm, Y M.
SO I replaced the startDoing function by:

function startScrapping(startup, tabId, key, total) {
    send(tabId, { command: 'Scrape Startup', url: startup.url, key: key, total: total });
}

and the send function is :


async function send(tabId, msg) {
  for (let i = 0; i < 2; i++) {
    try {
      return await chrome.tabs.sendMessage(tabId, msg);
    } catch (err) {
      await chrome.scripting.executeScript({
        target: { tabId },
        files: ['vendors/jquery.min.js', 'content/content.js'],
      });
    }
  }
}

all this in the background script.
Still the same issue.

My manifest is :

{
    "manifest_version": 3,
    "name": "My App",
    "short_name": "My App",
    "description": " My App",
    "version": "1.0",
    "action": {
        "default_icon": "icon.png",
        "default_title": " My App",
        "default_popup": "popup/popup.html"
    },
    "icons": {
        "16": "icon.png",
        "48": "icon.png",
        "128": "icon.png"
    },
    "permissions": [
        "tabs",
        "activeTab",
        "cookies",
        "storage",
        "webNavigation",
        "scripting"

    ],
    "background": {
        "service_worker": "background/background.js"
    },
    "host_permissions": ["<all_urls>"],

        "content_scripts": [{
        "matches": [
            "http://*/*",
            "https://*/*"
        ],
        "js": [
            "vendors/jquery.min.js",
            "content/content.js"
        ]
    }]
}


when you say "Another explanation may be that the message was sent by an old document in the tab, then the tab navigated to a new URL or it was reloaded, so the console was cleared (assuming you didn't enable "Preserve log" in console settings). If your content_scripts uses all_frames:true it could have been an iframe." since the script is launched with onUpdate I think it's started only when I load the new page?

If I cannot do it that way, is there another way of launching a specific script when the page loads?

Thank you for pointing out the return true, I will remove them.

I can try to get the documentID and send it to this document ID in particular, do you think that might fix the issue? I'm justr surprised I cannot get this interaction between the background and the content script to work in a reliable fashion... weird! Are there no other way around this?

Thanks

wOxxOm

unread,
May 10, 2024, 1:02:24 PMMay 10
to Chromium Extensions, Y M., wOxxOm
>  since the script is launched with onUpdate I think it's started only when I load the new page?

To actually inject on demand you need to remove content_scripts form manifest.json and reload the extension.

> I can try to get the documentID and send it to this document ID in particular, do you think that might fix the issue? I'm justr surprised I cannot get this interaction between the background and the content script to work in a reliable fashion... weird! Are there no other way around this?

documentId is such a method.

Another solution would be to use port-based communication and open the port from the content script via chrome.runtime.connect. It's somewhat trickier to do it right, though.

Y M.

unread,
May 13, 2024, 10:56:56 AMMay 13
to Chromium Extensions, wOxxOm, Y M.
Hello,

I followed your guidance but I'm still facing the same issue. I am clueless as to what is happening with my plugin.
Do you have any idea what else I should look at?

Many thanks

wOxxOm

unread,
May 13, 2024, 11:06:18 AMMay 13
to Chromium Extensions, Y M., wOxxOm
I can't really help without seeing the new code. When the cause is unclear use the basic debugging approach: divide-and-conquer. Remove half the code (half the functionality) and see if it works. If it does the problem is in the removed part. Keep halving the code in the part that contains the problem until the problem disappears. The previously removed part will contain the problem. Sometimes it's helpful to write a separate super simple extension that does just one thing to make sure it works, then add more things incrementally, each time verifying it still works.
Reply all
Reply to author
Forward
0 new messages