'How to handle "Unchecked runtime.lastError: The message port closed before a response was received"?

Forgive me for any glaring mistakes as I am new to chrome extensions, but this error with Chrome's message passing API has been discussed here, here, and here in the past and the common response is along the lines of 'disable existing Chrome extensions, one of them is causing the error'. Is this the best that can be accomplished? Are we supposed to just roll over and accept the fact that our extensions will conflict with others? Returning true or returning a Promise for the listener callback function and using sendResponse does not solve the problem for me.

Currently, I can only get the new value stored in chrome.storage.local (no errors) by disabling all other chrome extensions, removing the extension and loading back up the unpacked extension. The code interestingly only seems to work on developer.chrome.com, it doesn't work at all on the other "matches" URLs in manifest.json.

I think that there is some significance in the await and async operators in solving this issue but I am unsure how to properly implement it.

manifest.json:

{
    "manifest_version": 2,
    "name": "my extension",
    "version": "1.0",
    "description": "its my extension",
    "permissions": [
        "declarativeContent", 
        "storage", 
        "activeTab"
    ],
    "content_scripts": [
        {
          "matches": [
            "*://developer.chrome.com/*",
            "*://bbc.co.uk/*",
            "*://theguardian.com/*",
            "*://dailymail.co.uk/*"
          ],
          "js": ["content.js"]
        }
      ],
    "background": {
      "scripts": ["background.js"],
      "persistent": false
    },
    "content_security_policy": "script-src 'self' https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js; object-src 'self'",
    "page_action": {
        "default_popup": "popup.html"
    },
    "icons": {
        "16": "images/icon16.png",
        "32": "images/icon32.png",
        "48": "images/icon48.png",
        "128": "images/icon128.png"
      }
}

popup.html:

<!DOCTYPE html>
  <html>
    <head>
      <title>my extension</title>
      <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
      <script src="popup.js"></script>
      <link rel="stylesheet" type="text/css" href="style.css">
    </head>
    <body>
      <h1>my extension</h1>
      <h2>Article: <span id="article-headline"></span></h2>
      <button id="detect-article">Detect Article</button>
    </body>
  </html>

popup.js:

$(document).ready(function() {
    $("#detect-article").click(function() {
        chrome.tabs.query({active: true, currentWindow: true}, function(tabs){
            chrome.tabs.sendMessage(tabs[0].id, {request: "Requesting headline"}, function(response) {
                console.log("Requesting headline")
            });
        });
    });    
})

function getHeadline(changes) {
    let changedValues = Object.keys(changes);
    //console.log(changedValues);

    for (var item of changedValues) {
        console.log("new value: " + changes[item].newValue);
        $("#article-headline").text(changes[item].newValue)
    }
}

chrome.storage.onChanged.addListener(getHeadline);

content.js:

function handleRequest(message, sender, sendResponse) {
    console.log("Request recieved");
    let headlineList = document.getElementsByTagName("h1");
    chrome.storage.local.set({headline: headlineList[0].innerText}, function() {
        console.log("'" + headlineList[0].innerText + "' stored in local storage");
    });
    return true;
}

chrome.runtime.onMessage.addListener(handleRequest);

background.js:

chrome.runtime.onInstalled.addListener(function() {
    chrome.declarativeContent.onPageChanged.removeRules(undefined, function() {
      chrome.declarativeContent.onPageChanged.addRules([{
        conditions: [
          new chrome.declarativeContent.PageStateMatcher({
            pageUrl: { hostContains: 'developer.chrome.com' },
          }),
          new chrome.declarativeContent.PageStateMatcher({
            pageUrl: { hostContains: 'bbc.co.uk' },
          }),
          new chrome.declarativeContent.PageStateMatcher({
            pageUrl: { hostContains: 'theguardian.com' },
          }),
          new chrome.declarativeContent.PageStateMatcher({
              pageUrl: { hostContains: 'dailymail.co.uk' },
          }),
        ],
      actions: [new chrome.declarativeContent.ShowPageAction()]
    }]);
  });
});

Many thanks for taking the time to look/re-look at this issue, solutions pertaining to the aforementioned 'disable existing extensions' are not what I am looking for.



Solution 1:[1]

When you specify a callback for sendMessage you're telling the API that you NEED a response so when your content script doesn't respond using sendResponse the API thinks something terrible happened and reports it as such!

Reminder: when editing content scripts make sure to reload both the extension on chrome://extensions page and the tabs that should have this content script.

You don't need any response:

  • Remove the callback in sendMessage

    chrome.tabs.sendMessage(tabs[0].id, {request: "Requesting headline"});
    
  • Remove return true - all it does currently is telling the API to keep the messaging port open indefinitely, which will never be used by you, so it's just a memory leak source.

    chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
      // do something
      // don't return true
      // ManifestV2: don't call sendResponse
      // ManifestV3 bug: uncomment the next line
      // sendResponse();
    });
    

    For ManifestV3 in Chrome 99, 100, 101 you need a dummy sendResponse() call.

You need a response:

  • Replace return true with sendResponse

    chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
      sendResponse('whatever');
    });
    

You need a response from asynchronously running code such as chrome API callback:

  • Keep return true

  • Call sendResponse(someImportantData) inside the callback

    chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
      chrome.storage.local.set({foo: 'bar'}, () => {
        sendResponse('whatever');
      });
      return true;
    });
    

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1