Search code examples
javascriptgoogle-chrome-extensionchrome-extension-manifest-v3

Unable to send message from popup.js to content.js in chrome extension


I have been building an extension and trying to send a message to content script when a button in the popup is clicked but have been getting this error "Could not establish connection. Receiving end does not exist." and also sometime getting "Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'id')" in popup.js

// manifest.json file

{
  "manifest_version": 3,
  "name": "Extension",
  "version": "1.0",
  "action": {
    "default_popup": "popup.html"
  },
  "content_scripts": [
    {
      "js": ["content.js"],
      "matches": ["https://discord.com/channels/*"]
    }
  ],
  "permissions": ["tabs"],
  "background": {
    "service_worker": "background.js"
  },
  "host_permissions": ["https://discord.com/channels/*"]
}

   
    

// popup.js file.

const sendMessageBtn = document.querySelector("button");

sendMessageBtn.addEventListener("click", async () => {
  console.log("clicked");       // being logged to console
  const [tab] = await chrome.tabs.query({
    active: true,
    lastFocusedWindow: true,
  });
  const response = await chrome.tabs.sendMessage(tab.id, { greeting: "hello" });
  // do something with response here, not outside the function
  console.log(response);
});

When clicking on the button getting error: "Could not establish connection. Receiving end does not exist." in extension error logs

// content.js file

console.log("content script running"); // being logged to console
chrome.runtime.onMessage.addListener(
     async function(request, sender, sendResponse) {
      console.log(sender.tab ?
                  "from a content script:" + sender.tab.url :
                  "from the extension");
                //   console.log(request);
      if (request.greeting === "hello")
         await sendResponse({farewell: "goodbye"});
    }
  );
        

// popup.html file

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Extension</title>
    <link rel="stylesheet" href="popup.css" />
  </head>
  <body>
    <h1>extension</h1>

    <input id="message-field" type="text" placeholder="enter message" />
    <button>Send message</button>

    <script src="popup.js"></script>
  </body>
</html>

Solution

  • There are several problems.

    • A bug in Chrome when devtools for the popup is focused breaks chrome.tabs.query, more info.

    • chrome.runtime.onMessage listener can't be declared with async in Chrome, more info.

    • When the extension is installed/updated its new content script won't be automatically injected into the currently opened tabs in Chrome/ium, so you'll have to do it explicitly yourself as shown here, but there's a much better alternative in cases like yours where the access to the page is necessary only on demand after the user invoked the extension: programmatic injection via executeScript.

      • remove content.js
      • remove content_scripts from manifest.json
      • remove "tabs" from "permissions" - it's not necessary and now there'll be no warning about observing the browser history.
      • add "scripting" to "permissions".
    document.querySelector('button').onclick = async () => {
      // workaround for devtools window https://crbug.com/462939
      const { id: windowId } = await chrome.windows.getCurrent();
      const [tab] = await chrome.tabs.query({ active: true, windowId });
      let result;
      try {
        [{ result }] = await chrome.scripting.executeScript({
          target: { tabId: tab.id },
          // this runs in the web page, so it can't see variables in scope like `tab` 
          func: (param1, param2) => {
            return document.documentElement.innerHTML;
            // return {it: 'can', be: ['an object', 'too']};
          },
          // this is sent to the func's param1 and 2
          args: [123, { foo: 'bar' }],
        });
      } catch (err) {
        // the tab is not injectable
        console.warn(err);
        return;
      }
      console.log(result); // printing in the popup's devtools console
    };