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?
The runtime. lastError property is set when an asynchronous function has an error condition that it needs to report to its caller. If you call an asynchronous function that may set lastError , you are expected to check for the error when you handle the result of the function.
Chrome throws Unchecked runtime. lastError: The message port closed before a response was received when either there is a conflict with an extension or some of the onMessage listeners in your created extension is expected to return a promise.
Description. Use the chrome. runtime API to retrieve the background page, return details about the manifest, and listen for and respond to events in the app or extension lifecycle. You can also use this API to convert the relative path of URLs to fully-qualified URLs.
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.
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'));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With