56

My chrome extension has the following two javascripts:

background.js, running as background script:

chrome.runtime.onMessage.addListener(function(message, sender, sendResponse) {
    if (message.data == "takeScreenshot") {
        var resp = sendResponse;
        chrome.tabs.captureVisibleTab(function(screenshotUrl) {
            resp({
                screenshot: screenshotUrl
            });
        });
        return true; // Return true to tell that the response is sent asynchronously
    } else {
        return "TestReply";
    }
});

api.js, running as web accessible resource:

window.takeScreenshot = (function() {
    var isTakingScreenshot = false; // Semaphore
    return function() {
        if(isTakingScreenshot) return Promise.reject();
        isTakingScreenshot = true;
        return new Promise(function(resolve, reject) {
            chrome.runtime.sendMessage("eomfljlchjpefnempfimgminjnegpjod", "takeScreenshot", function(response) {
                console.log(response);
                isTakingScreenshot = false;
                resolve(response.screenshot);
            });
        });
    }
})()
window.test = (function() {
    return function() {
        return new Promise(function(resolve, reject) {
            chrome.runtime.sendMessage("eomfljlchjpefnempfimgminjnegpjod", "test", function(response) {
                console.log(response);
                resolve(response.length);
            });         
        });
    }
})();

When I execute in a tab's console either function (auto-complete knows them, so they are available), I get the error:

Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.

and the respone returned is undefined.

I have checked that the id in sendMessage is the same as in the manifest and in the chrome://extensions page, and I have opened the background page DevTools of the extension and manually added the same listener there to make sure the listener is indeed registered.

My searches found that this error means the listener has not been correctly registered, but I don't find the underlying reason. Do you have an idea what causes this error?

ted
  • 13,596
  • 9
  • 65
  • 107
Alexander
  • 19,906
  • 19
  • 75
  • 162
  • 2
    Since you're using a web_accessible_resource you must be running it as a page script which doesn't have access to the extension environment - you need to expose messaging via [externally_connectable](https://developer.chrome.com/extensions/manifest/externally_connectable) key. – wOxxOm Jan 14 '19 at 14:27
  • 1
    @wOxxOm could you explain a bit about the how `externally_connectable` is different from general `permissions`? It seemed like I was able to use `chrome.runtime.sendMessage()` and `chrome.runtime.onMessage.addListener()` without any problems for years without understanding the `externally_connectable` setting. – Atav32 Feb 11 '19 at 21:01
  • @Atav32, I don't see how your comment relates to the question and my comment so I can't answer it, but I can guess you were not running a page script. – wOxxOm Feb 12 '19 at 04:38
  • we had the same error and we were not trying to communicate with external extensions. I have a non optimal solution below. Maybe @wOxxOm has a better idea. There is no other api to check if the port is being listened to already before calling connect. – David Dehghan Feb 14 '19 at 08:59
  • I couldn't guess based on the bits of info provided in the question, comments, and the answer. Maybe you can upload a demo extension or the original extension with the exact steps to reproduce the problem and I'll have a look. – wOxxOm Feb 14 '19 at 13:06
  • 4
    Hi @wOxxOm - I made a minimal extension to show you the problem. If you just load this extension you will get the error "Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist." but if you open the popup and then reload the content script the problem goes away. https://github.com/ddehghan/bugReproExtension . How can the content script ensure that there is a listener before connecting. This seems like a bug. – David Dehghan Mar 06 '19 at 01:58

8 Answers8

46

OK. I found out what the problem is. This is a change in chromes behavior since 72 I guess. The problem is if you try to call chrome.runtime.connect() before you have opened a channel on the other end in Background or popup page then you will get that error.

What Chrome docs say is that you can send a message immediately. In the past this would just work and the messages would get either delivered or dropped. But now it is failing.

chrome docs: Upon calling tabs.connect, runtime.connect or runtime.connectNative, a Port is created. This port can immediately be used for sending messages to the other end via postMessage.

So our workaround is to make sure the connection listener is set up first before calling connect() by just delaying the connect() call:

chrome.runtime.onConnect.addListener(port => {
  console.log('connected ', port);

  if (port.name === 'hi') {
    port.onMessage.addListener(this.processMessage);
  }
});

If you set up a listener for the disconnect event on the content script side, it actually gets called when you try to chrome.runtime.connect and you don't have anything listening on the other end. Which is correct behavior according the Port lifetime

port = chrome.runtime.connect(null, {name: 'hi'});      
port.onDisconnect.addListener(obj => {
  console.log('disconnected port');
})

I don't know if there is a way to avoid this other than with setTimeout and trying to get the chrome.runtime.connect to come after chrome.runtime.onConnect.addListener is called. This is not a good solution because it leads to timing errors. Maybe another workaround is to reverse the direction of the channel. And initiate the connect from popup instead of contentscript.

Update: I made a minimum repro extension for this issue.

Sam Dutton
  • 14,775
  • 6
  • 54
  • 64
David Dehghan
  • 22,159
  • 10
  • 107
  • 95
  • 1
    I made a minimum repro extension for testing. – David Dehghan Mar 06 '19 at 02:02
  • @DavidDehghan This is definitely a bug! Have you made a [bug report](https://crbug.com)? – Jack Steam Jun 19 '19 at 21:30
  • I didn’t. You can go ahead and do that and use my repro git. Chrome team is very slow in looking at these type of regression issues. It will probably take them 6 months at the earliest. If you create a bug link it here so we can all star it. – David Dehghan Jun 19 '19 at 21:36
  • On further thought, I think this behavior is intentional. Since the port fires `onDisconnect` immediately, it will emit no further events. It's dead, in other words. – Jack Steam Jun 20 '19 at 15:51
  • 3
    No it is a bug. The doc says it should be available immediately for sending. Which means internally it should block the call until the connection is established. It is a bad api design and it is implemented even worse. – David Dehghan Jun 20 '19 at 16:51
  • I wonder what have been the design considerations leading to those runtime errors which cannot be caught using the standard try-catch mechanism while initiating the connection. – matanster Sep 06 '21 at 03:45
17

Here's a pattern I use to solve this. Content script tries to reach background script. If background script is not available yet then we can see that from chrome.runtime.lastError being set (instead of being undefined). In this case try again in 1000 ms.

contentScript.js

function ping() {
  chrome.runtime.sendMessage('ping', response => {
    if(chrome.runtime.lastError) {
      setTimeout(ping, 1000);
    } else {
      // Do whatever you want, background script is ready now
    }
  });
}

ping();

backgroundScript.js

chrome.runtime.onConnect.addListener(port => {
  port.onMessage.addListener(msg => {
    // Handle message however you want
  });
});

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => sendResponse('pong'));
patridge
  • 26,385
  • 18
  • 89
  • 135
Márton György
  • 321
  • 3
  • 8
  • While this code snippet may be the solution, [including an explanation](//meta.stackexchange.com/questions/114762/explaining-entirely-‌​code-based-answers) really helps to improve the quality of your post. Remember that you are answering the question for readers in the future, and those people might not know the reasons for your code suggestion. – Johan Jun 27 '19 at 09:08
  • i would like to add that the pattern suggested is really useful in the case of "simple one-time requests". For long-lived connections the "port" mechanism is the way to go. See https://developer.chrome.com/extensions/messaging. – ksridhar Jul 17 '19 at 14:37
  • actually it uses "simple one-time requests" to establish a long-lived port connection – Márton György Jul 17 '19 at 18:01
  • Thank you, been looking for a solution for a week!! – Kareem Elsayed Nov 23 '19 at 02:11
  • This is great. I think for me I had switched around the background.js and content.js scripts. – Neophile Sep 14 '20 at 11:15
  • Note: For me, this message was simply because I was sending to a non-existent extension ID (i.e. extension not installed or disabled) (I'm sending to an extension that might be disabled). So blindly retrying is not a good idea, it could lead to infinite loop. – jakub.g Aug 22 '22 at 19:45
17

Some security changes in chrome seems like you have to take a slightly different approach when messaging between content scripts and the background script.

The calls to use are

  1. From background page:

chrome.runtime.onMessageExternal.addListener Notice the External part.

  1. The manifest.json needs extra permissions
      "externally_connectable": {
        "ids": ["abcdefghijklmnopqrstuvwxyzabcdef"],
        "matches": ["https://example.com/*"],
        "accepts_tls_channel_id": false
      },

"abcdefghijklmnopqrstuvwxyzabcdef" is your extension id.

"https://example.com/*" is the domain the content script runs on.

  1. From the content script:

    chrome.runtime.sendMessage / chrome.runtime.connect with a extensionId as the first parameter.

Read more here https://developer.chrome.com/extensions/manifest/externally_connectable

TrophyGeek
  • 5,902
  • 2
  • 36
  • 33
4

I'm adding this here because I had this error and NOT A SINGLE StackOverflow post solved my issue. My problem was because my background.js was sending a bunch of messages to my content script for just 1 visit of a webpage. Once I added a sender.status check, it only sent the message when the tab change was "completed", and the error I had went away.

chrome.tabs.onUpdated.addListener(function(activeInfo, sender, sendResponse) {
    // conditional check prevents sending multiple messages per refresh
    if(sender.status ===  "complete") {
      // do stuff here
    }
});
George
  • 120
  • 8
  • I think the `status` property of the callback's second argument has been removed. I can't find it in the docs: https://developer.chrome.com/docs/extensions/reference/runtime/#type-MessageSender – Protector one Nov 21 '22 at 23:44
3

If you're getting this error message while developing a Chrome Extension be sure to disable other Chrome extensions you have installed in your browser. In particular the "AdBlock" extension seemed to interfere with messaging and resulted in my extension throwing this error. Disabling AdBlock resolved this issue for me.

clhenrick
  • 798
  • 6
  • 13
  • Not necessarily. Adblocks aren't the reason for this error. It is mostly because of recent changes in chrome browser (v 77) – Niraj Kumar Apr 30 '20 at 15:58
1

After study more the solution of my extension, I had consider change the Long-lived connection to Simple one-time requests, Serously, I had spend two days search for a solution and do not can found a solutin for this problem in my extension. The popup is make with ReactJs. After remove all runtime.connect and runtime.onConnect, change the api and use onMessage, and only use callback of addListner to handle messages, the problem desapeared.I did this, because I don't need more real time changes and change it with requests only, think about that, if you do not need Long-lived connection change it for Simple one-time requests, for more information. I know that it is not a solution, but for everyone that needs accelerate or finish the development this is what I did.

My solution:

// content_script.js
if (!chrome.runtime.onMessage.hasListeners()) {
  window.chrome.runtime.onMessage.addListener(
    (request, sender, sendResponse) => {
      if (request?.type === 'load_titles') {
        sendResponse({
          type: 'loaded_titles',
          titles,
        });
      }
      if (request?.type === 'anything_else') {
        // do anything else
      }
    },
  );
}

If you need use sendResponse asyncronous to use in another function, you need return true inside on callback of onMessage.addListner.

Function responsable for send and receive messages:

 // used in app.js on popup
 export function messager(data, callback) {
  window.chrome?.tabs?.query({ url: 'https://example.com/' }, tabs => {
    const tabId = tabs[0].id;
    window.chrome.tabs.sendMessage(tabId, data, callback);
  });
}

 // app.js
 const loadTitles = useCallback(() => {
    messager({ type: 'load_titles' }, response => {
      if (response?.type === 'loaded_titles') {
         console.log(response.titles)
        // do anything you want
      }
    });
  }, []);
Dharman
  • 30,962
  • 25
  • 85
  • 135
  • I got the same error using the "Simple one-time requests" :) – Amit Sharma May 10 '21 at 12:08
  • Same. Using simple one-time requests, no connect, tried returning true or not, but still getting `Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.` - Chromium: 92.0.4515.131 – kev Aug 15 '21 at 04:22
0

I encountered this error while developing the Chrome extension. For me, the issue was a different extension ID. If you need to use your extension ID within your code (while sending messages to the background worker, for example), make sure that your extension ID matches the one in the browser.

Dan
  • 97
  • 1
  • 6
0

re: @david-dehghan's solution

I don't know if there is a way to avoid this other than with setTimeout and trying to get the chrome.runtime.connect to come after chrome.runtime.onConnect.addListener is called.

I believe I have a solution to defer setTimeout on chrome.runtime.connect unless necessary.

As @david-dehghan's solution described, we also ran into the issue where calling chrome.runtime.connect triggers the chrome.runtime.onDisconnect handler which results in the Could not establish connection error and the connection not being created.

Instead of delaying the initial chrome.runtime.connect call, we can add retry logic to retry chrome.runtime.connect from the event handlers by observing lastError. Seems to be working alright.

example of the content script (relative to the same background script example above):

let extensionPort;

const setupFunction = () => {
  extensionPort = chrome.runtime.connect({ name: 'hi' });
  extensionPort.onDisconnect.addListener(onDisconnectListener);
}

const onDisconnectListener = () => {
  const lastError = chrome.runtime.lastError;

  extensionPort.onDisconnect.removeListener(onDisconnectListener);

  if (lastError) {
    setTimeout(setupFunction, 1000);
  }
}
digiwand
  • 1,258
  • 1
  • 12
  • 18