Chrome Extension message passing: Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist
Solution 1:
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.
Solution 2:
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'));
Solution 3:
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
- From background page:
chrome.runtime.onMessageExternal.addListener
Notice the
External part.
- 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.
-
From the content script:
chrome.runtime.sendMessage
/chrome.runtime.connect
with aextensionId
as the first parameter.
Read more here https://developer.chrome.com/extensions/manifest/externally_connectable
Solution 4:
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.
Solution 5:
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
}
});
}, []);