javascriptgoogle-chromegoogle-chrome-extensioncontent-scriptbrowser-extension

In Chrome extension, content script does not receive message from background on initial load


In my Chrome extension, the content script is supposed to receive a message from the background script on page load. But when I open a new tab and enter URL https://example.com, it does not receive the message. However, it works on page reload. Is it because page loaded from chrome://newtab?

// background script
chrome.runtime.onMessage.addListener(function (message, sender) {
  if (message.action === "NOTIFY_CONTENT_SCRIPT_LOADED") {
    console.log(`### ACK content script in page: ${message.pageURL} and tab: ${sender.tab.url}`);

    chrome.tabs.sendMessage(sender.tab.id, { action: "CHANGE_BG" }, () => {
      console.log("### ACK changed BG color of client page");
      // do more stuff
    });
  }
});
// content script
chrome.runtime.onMessage.addListener((message, _, sendResponse) => {
  if (message.action === "CHANGE_BG") {  // <---- This is not received on first page load
    document.body.style.background = "yellow";
    sendResponse();
  }
});

console.log("### Notifying background from page: " + location.href);
chrome.runtime.sendMessage({ action: "NOTIFY_CONTENT_SCRIPT_LOADED", pageURL: location.href });

Console log from background script:

### ACK content script in page: https://example.com/ and tab: chrome://newtab/

Please note that the sender.tab.url is chrome://newtab/ instead of https://example.com/. This is intermittently happening. That is, does not happen in some cases:

I have created an example extension with same issue: https://github.com/lazyvab/chrome-newtab-issue


Edit: Tried suggestion from comment -

In case of multiple messages don't use sendMessage, but switch to chrome.runtime.connect.

// content script
const bgPort = chrome.runtime.connect();
bgPort.onMessage.addListener((msg) => {
  console.log("### Received BG msg", msg);
});

// background script
chrome.runtime.onConnect.addListener((port) => {
  port.onDisconnect.addListener((...args) => {
    console.log("### disconnected port on tab", tabId, ...args);
  });

  console.log("### BG: sending ping to tab", tabId);
  port.postMessage({ action: "ping" });
});
  

I opened https://example.com in new tab and here is the result.

A screenshot from background page console: screenshot from background page console

A screenshot from content script (page) console: screenshot from content script

As can be seen in the screenshots, the port disconnects immediately after website loads and background script is not able to send messages using the port again.


Solution

  • I could finally solve this issue by establishing a long-lived port connection between content script and background script in case of pre-rendered pages.

    // background.js
    let clientPort = null;
    let clientLoadSubscribers = [];
    
    chrome.runtime.onConnect.addListener((port) => {
        const tabId = port.sender.tab.id;
    
        if (port.sender.documentLifecycle === "active") {
          clientPort = port;
          clientLoadSubscribers.forEach((subscriber) => subscriber());
          clientLoadSubscribers = [];
        }
    
        port.onDisconnect.addListener(() => {
          clientPort = null;
    
          chrome.tabs.executeScript(
            tabId,
            {
              code: "chrome.runtime.connect()",
            },
            () => {
              if (chrome.runtime.lastError) {
                // ignore
              }
            }
          );
        });
    });
    
    const sendMessageToClient = (tabId, ...restArgs) => {
      const send = () => chrome.tabs.sendMessage(tabId, ...restArgs);
    
      if (clientPort) {
        send();
      } else {
        clientLoadSubscribers.push(send);
      }
    };
    
    // content script
    chrome.runtime.connect();
    

    This way I could reliably send messages from background to content script using sendMessageToClient().