Popup SendMessage to Background Service Worker (MV3) Cannot Establish Connection - google-chrome-extension

Update
It seems like webpack is causing the issues.
If I replace the dist/background.js with:
console.log("background is running"); // Now visible ✅
const handler = (req, sender, sendResponse) => {
switch (req.type) {
case "message":
sendResponse({ data: "hi" });
break;
default:
break;
}
};
chrome.runtime.onMessage.addListener(handler);
Both the console log (in service worker) and response (in popup) are observed. Also, there are no errors.
Time to investigate further
Update #2
Upon further inspection, I noticed that the webpack output is wrapped in a function, but never called:
{
/***/ "./src/background.ts":
/*!***************************!*\
!*** ./src/background.ts ***!
\***************************/
/***/ (function () {
console.log("background is running");
const handler = (req, sender, sendResponse) => {
switch (req.type) {
case "message":
sendResponse({ data: "hi" });
break;
default:
break;
}
};
chrome.runtime.onMessage.addListener(handler);
/***/
}) // <==== adding () will make it an IIFE and everything works!
},
Question is how to automate this?
Update #3
Seems like the IIFE trick I mentioned above only works when there are no imports in background.js. As soon as I add any import, I get an error that the background script is not valid.
Adding module type property to background does not help:
// manifest.json
{
...
"background": {
"service_worker": "background.js",
"type": "module"
},
...
}
Update #4
Turns out this was caused by vendor splitting optimization in webpack:
// webpack.config.json
{
...
optimization: {
runtimeChunk: "single",
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: "vendors",
enforce: true,
chunks: "all",
},
},
},
},
...
}
Once I removed this, everything started working properly!
Would be nice to keep this around, but it is just an optimization after all, so if it breaks things, best to get rid of it.
How I figured this out? Well as I mentioned, everything worked a couple of commits ago. Back then I didn't have this optimization, so I tried removing it again, and everything started working again like magic.
Original Question
I had this working previously, so I am sure my setup is correct, but regardless of what I try, I now get
Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.
Also, I cannot find a solution online which indicates anything that differs from my setup.
Here is a MWE
dist folder structure:
dist/background.js
dist/index.html
dist/manifest.json
dist/popup.js
dist/runtime.js
dist/vendors.js
other misc files
// manifest.json
{
...
"background": {
"service_worker": "background.js"
},
...
}
// src/components/App.tsx
export default function App(): JSX.Element {
...
useEffect(() => {
chrome.runtime.sendMessage({ type: 'someMessage' }, ({ data }) => {
console.log(data);
});
}, []);
...
}
// src/background.ts
import { TSentResponse } from "./typings/background";
import { executeResponse } from "./utils/background";
console.log('sanity check'); // <=== does not fire 🤔
// also doesn't seem to be called 😥
const handleMessage = (req: { type: string }, sender: chrome.runtime.MessageSender, res: (response?: unknown) => void) => {
switch (req.type) {
case 'someMessage':
// an IIFE (worked fine before)
break;
default:
break;
}
return true; // due to asynchronous nature
};
chrome.runtime.onMessage.addListener(handleMessage);
My service worker is registered properly:
When I said above that I cannot see the logs for background, I mean when I check in the service worker dev tools (from above image), not the popup dev tool.
When I open the popup, I get the following errors:
Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.
Error handling response: TypeError: Cannot destructure property 'data' of 'undefined' as it is undefined.
I also don't see the service worker actually being registered - it did register before...
Is this a bug with MV3?
My repository (not fully up to date, but can be used to quickly check the above)

Follow these steps:
To send a message from the popup.js to the background service worker, first you need to get current tab id. to get the current tab id do as below:
popup.js
const messageKey = 'key-message-from-popup-to-background';
// Listen to get current tab info from the content_popup
document.addEventListener('_listener_getCurrentTabInfo', function (e) {
const tab = e.detail.response;
// Send a message to the background,js
chrome.tabs.sendMessage(tab.id, messageKey);
});
// Send message to content_popup to get current tab info
document.dispatchEvent(new CustomEvent('_dispatch_getCurrentTabInfo'));
content_popup.js
const config = {
scripts: [
// add ".js" files to web_accessible_resources in manifest.json
"popup/popup.js"
]
};
// Listen to get current tab info from the background.js
document.addEventListener('_dispatch_getCurrentTabInfo', function (e) {
// Key to help
const type = 'get_current_tab_info';
// Send a message to the background.js to get current tab info
chrome.runtime.sendMessage({type: type}, response => {
document.dispatchEvent(new CustomEvent('_listener_getCurrentTabInfo', {detail: {'response': response}}));
});
});
// prepare and add scripts
var scriptList = config['scripts'];
for (var i = 0; i < scriptList.length; i++) {
var s = document.createElement('script');
s.src = chrome.runtime.getURL(scriptList[i]);
s.onload = function () {
this.remove();
};
(document.head || document.documentElement).appendChild(s);
}
background.js
// Get messages
chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) {
// If request related to fetch data from API
if (request.url && request.url !== '') {
// Fetch http request and send back the response
fetch(request.url, request.init).then(function (response) {
return response.text().then(function (text) {
sendResponse([{
body: text,
status: response.status,
statusText: response.statusText,
}, null]);
});
}, function (error) {
sendResponse([null, error]);
});
// If request do not related to fetch data from API
} else {
detectMessageType(request, sender, sendResponse);
}
return true;
});
function detectMessageType(request, sender, sendResponse) {
// Check background request type
if (request && request.type) {
switch (request.type) {
case 'get_current_tab_info': {
getCurrentTabInfo(tab => {
// Send current tab info back to content_popup
sendResponse(tab);
});
break;
}
}
}
});
function getCurrentTabInfo(callback) {
chrome.tabs.query({
active: true,
currentWindow: true
}, function (tab) {
callback(tab[0]);
});
}
Get message in contentScript
chrome.runtime.onMessage.addListener((message, sender, response) => {
const message = message;
// Write your codes
});
This is the project structure (partial)
Project
-- popup
---- popup.js
---- content_popup.js
---- popup.html
---- popup.css
-- content_extension.js
-- background.js
-- manifest.js
Add the following code to the end of the body tag inside the popup.html file
<script src="content_popup.js"></script>
Update the manifest.json file like below
"manifest_version": 3,
.
.
.
"content_scripts": [
{
"js": [
"content_extension.js"
]
}
],
"action": {
"default_icon": ...,
"default_title": ...,
"default_popup": "popup/popup.html"
},
"background": {
"service_worker": "background.js"
},
"web_accessible_resources": [
{
"resources": [
"popup/popup.html",
"popup/popup.js"
]
}
],
...

Related

chrome.webRequest.onBeforeRequest didn't work in manifest version 3

In my chrome extension with manifest V2 I was using chrome.webRequest.onBeforeRequest to get all requests of current tab. Here is
const dataSet = {};
chrome.webRequest.onBeforeRequest.addListener(function (details) {
if (details && details.url && details.type == "image") {
if (!dataSet[tabId]) {
dataSet[tabId] = new Set([]);
}
const currentSet = dataSet[tabId];
currentSet.add(details.url);
}
}, {
urls: ["<all_urls>"]
});
I'm trying same code in manifest version 3 but event didn't triggers. Also I've tried this workaround but it still didn't works.
chrome.webNavigation.onBeforeNavigate.addListener(function(){
// this event is not being triggered
chrome.webRequest.onBeforeRequest.addListener(function(details){
},{urls: ["<all_urls>"],types: ["main_frame"]});
},{
url: [{hostContains:"domain"}]
});
Also tried to use webNavigation.onHistoryStateUpdated but still onBeforeRequest didn't triggers
chrome.webNavigation.onHistoryStateUpdated.addListener((details) => {
console.log('wake me up', details);
chrome.webRequest.onBeforeRequest.addListener(
(details) => {
console.log(details);
},
{
urls: ['<all_urls>'],
},
);
});
Console output of background page

Chrome Extension V3 Set Icon [duplicate]

I'm trying to develop a simple chrome extension. There is a pageAction's default icon that should appear on the pages with a specific URL (http://www.example.com/*).
There is a two file
manifest.json
{
"manifest_version": 2,
"name": "name",
"description": "description",
"version": "1.0",
"background": {
"scripts": [
"background.js"
],
"persistent": false
},
"page_action": {
"default_icon" : "images/icons/19.png"
},
"permissions": [
"declarativeContent"
]
}
background.js
chrome.runtime.onInstalled.addListener(function () {
chrome.declarativeContent.onPageChanged.removeRules(undefined, function () {
chrome.declarativeContent.onPageChanged.addRules([
{
// rule1
conditions : [
new chrome.declarativeContent.PageStateMatcher({
pageUrl : {urlPrefix : 'http://www.example.com/'}
})
],
actions : [
new chrome.declarativeContent.ShowPageAction()
]
},
{
// rule2
conditions : [
new chrome.declarativeContent.PageStateMatcher({
pageUrl : {queryContains : 'q1=green'}
})
],
actions : [
new chrome.declarativeContent.SetIcon({
path : {"19" : "images/icons/green.png"}
})
]
}
]);
});
});
rule1 should show pageAction's icon and rule2 should change icon to green version on the pages with URL that looks like http://www.example.com/?q1=green
But during installation of extension things come to:
Error in response to events.removeRules: Error: Invalid value for argument 1. Property '.0': Value does not match any valid type choices.
I dug deeply into this error, and it seems like the documentation does not reflect well the fact that using path parameter is not implemented. This is certainly a bug, tracked here.
For now, to fix this you need to load the image and convert it to ImageData format before calling SetIcon.
// Takes a local path to intended 19x19 icon
// and passes a correct SetIcon action to the callback
function createSetIconAction(path, callback) {
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
var image = new Image();
image.onload = function() {
ctx.drawImage(image,0,0,19,19);
var imageData = ctx.getImageData(0,0,19,19);
var action = new chrome.declarativeContent.SetIcon({imageData: imageData});
callback(action);
}
image.src = chrome.runtime.getURL(path);
}
chrome.declarativeContent.onPageChanged.removeRules(undefined, function () {
createSetIconAction("images/icons/green.png", function(setIconAction) {
chrome.declarativeContent.onPageChanged.addRules([
/* rule1, */
{
conditions : [
new chrome.declarativeContent.PageStateMatcher({
pageUrl : {queryContains : 'q1=green'}
})
],
actions : [ setIconAction ]
}
]);
});
});
If needed, this can be generalized to support high-DPI icon (19 + 38):
function createSetIconAction(path19, path38, callback) {
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
var image19 = new Image();
image19.onload = function() {
ctx.drawImage(image19,0,0,19,19); // fixed
var imageData19 = ctx.getImageData(0,0,19,19);
var image38 = new Image();
image38.onload = function() {
ctx.drawImage(image38,0,0,38,38);
var imageData38 = ctx.getImageData(0,0,38,38);
var action = new chrome.declarativeContent.SetIcon({
imageData: {19: imageData19, 38: imageData38}
});
callback(action);
}
image38.src = chrome.runtime.getURL(path38);
}
image19.src = chrome.runtime.getURL(path19);
}
In fact, you can use new chrome.declarativeContent.SetIcon({ path:'yourPath.png' }),
No need to specify size path: {"19": "images/icons/green.png"}, its default value is: 16
Use declarativeContent.SetIcon need to pay attention to a problem, it is actually a bug.
Actual use of path will eventually be automatically converted to ImageData.
see screenshot:
The root cause of the error of declarativeContent.SetIcon is: it is an asynchronous API, but at the same time it has no asynchronous callback. The only thing you can do is wait.
const action = new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' });
console.log(action.imageData); // => undefined
see screenshot:
// invalid
new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' }, action => console.log(action));
It takes a while to wait:
const action = new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' });
setTimeout(() => {
console.log(action.imageData); // {16: ArrayBuffer(1060)}
}, 5);
see screenshot:
When you understand the reason for the error of SetIcon, the problem will be solved well.
You only need to put the operation of addRules in the event.
onInstalled event
const rule2 = { id: 'hideAction', conditions: [...], actions: [new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' })]};
chrome.runtime.onInstalled.addListener(() => {
chrome.declarativeContent.onPageChanged.removeRules(undefined, () => {
chrome.declarativeContent.onPageChanged.addRules([rule2]);
});
});
pageAction.onClicked
const rule2 = { id: 'hideAction', conditions: [...], actions: [new chrome.declarativeContent.SetIcon({ path: 'assets/icon.png' })]};
chrome.pageAction.onClicked.addListener(() => {
if (condition) {
chrome.declarativeContent.onPageChanged.removeRules(['hideAction']);
} else {
chrome.declarativeContent.onPageChanged.addRules([rule2]);
}
});
There are some related information:
SetIcon source code
declarativeContent.SetIcon = function (parameters) {
// TODO(devlin): This is very, very wrong. setIcon() is potentially
// asynchronous (in the case of a path being specified), which means this
// becomes an "asynchronous constructor". Errors can be thrown *after* the
// `new declarativeContent.SetIcon(...)` call, and in the async cases,
// this wouldn't work when we immediately add the action via an API call
// (e.g.,
// chrome.declarativeContent.onPageChange.addRules(
// [{conditions: ..., actions: [ new SetIcon(...) ]}]);
// ). Some of this is tracked in http://crbug.com/415315.
setIcon(
parameters,
$Function.bind(function (data) {
// Fake calling the original function as a constructor.
$Object.setPrototypeOf(this, nativeSetIcon.prototype);
$Function.apply(nativeSetIcon, this, [data]);
}, this)
);
};
Discussion of related issues:
http://crbug.com/415315
No solution
As the guys before me mentioned, this is a bug. There are no solutions, only workarounds.
Workarounds
#1: Draw icon using canvas
As Xan described in his answer already.
#2 Wait for icon load (timeout hack)
Thanks to weiya-ou's answer I realized that I can just wait for the async icon data transformation to finish.
// Make your handler `async`
chrome.runtime.onInstalled.addListener(async () => {
const action = await new chrome.declarativeContent.SetIcon({
path: {
19: 'images/19.png',
38: 'images/38.png',
},
})
// THE WAIT STARTS
// Wait max. 10 loops
for (let i = 0; i < 10; i++) {
// Create a promise
const checkAvailability = new Promise((resolve) => {
// Resolve promise after 100ms
setTimeout(() => resolve(!!action.imageData), 100)
})
// Wait for the promise resolution
const isAvailable = await checkAvailability
// When image available, we are done here
if (isAvailable) break
}
// THE WAIT ENDS
const condition = new chrome.declarativeContent.PageStateMatcher({
pageUrl: { hostEquals: 'my.page.net' },
})
chrome.declarativeContent.onPageChanged.removeRules(undefined, () => {
chrome.declarativeContent.onPageChanged.addRules([
{
conditions: [condition],
actions: [action],
},
]);
});
});
#3 Use chrome.tabs
You would need the tabs permission (as said here).
chrome.tabs.onUpdated.addListener((tabId, { status }, { url }) => {
// Only check when URL is resolved
if (status !== 'complete') return
// What is our target page?
const isOurPage = url?.match(/my\.page\.net/)
if (isOurPage) {
// Show active icon
chrome.pageAction.setIcon({
path: {
19: 'images/19.png',
38: 'images/38.png',
},
tabId,
})
} else {
// Show inactive icon
chrome.pageAction.setIcon({
path: {
19: 'images/19-inactive.png',
38: 'images/38-inactive.png',
},
tabId,
})
}
})

Chrome extension send message from onInstall in backgroundjs to own extension javascript code

I need to check when the extension is installed and change my React state accordingly.
I use chrome.runtime.onInstalled on my background.js where I sendMessage to my react code - which is the content script of my extension.
background.js
async function getCurrentTab() {
let queryOptions = { active: true, currentWindow: true };
let [tab] = await chrome.tabs.query(queryOptions);
return tab;
}
chrome.runtime.onInstalled.addListener((details) => {
if (details?.reason === 'install') {
console.log('installed backgroundjs')
const tab = await getCurrentTab()
chrome.tabs.sendMessage(tab.id, { target: 'onInstall' })
openNewTab()
}
})
In my react Component - Dashboard.js
useEffect(() => {
if (extensionApiObject?.runtime) {
chrome.runtime.sendMessage({ target: 'background', message: 'check_installation', })
console.log('extension')
chrome.runtime.onMessage.addListener(handleMessage)
}
return () => {
if (extensionApiObject?.runtime) {
chrome.runtime.onMessage.removeListener(handleMessage)
}
}
})
function handleMessage(msg) {
console.log('handle messag func', msg)
if (msg.target === 'onInstall') {
console.log('extension on Install')
setShowWelcomeMessage(true)
}
}
What confuse me is that I already have a similar implementation for a different message that works without problem but in there I listen to chrome.runtime.onMessage() not chrome.runtime.onInstalled()
I wonder if I misunderstand how onInstalled method work and I cannot sendMessage from it?
UPDATE:
I change my code as suggested by #wOxxOm, I used chrome.tabs.sendMessage but still no luck.
chrome.runtime.onInstalled doesn't take as argument req, sender, sendResponse like other listener and I wonder if that means it will not be able to send a message from there :/
After #wOxxOm suggestions, I ended up removing the sendMessage solution and simply add an extra parameter to the url whenever I open a new tab after installation:
background.js
function openNewTab(param) {
chrome.tabs.create({
url: param ? `chrome://newtab?${param}` : 'chrome://newtab',
})
}
chrome.runtime.onInstalled.addListener((details) => {
if (details?.reason === 'install') {
chrome.tabs.sendMessage(tab.id, { target: 'onInstall' })
openNewTab('installed')
}
})
on my web app I just need to check the param and I can decide which UI to show

"Extension context invalidated" error when calling chrome.runtime.sendMessage()

I have a content script in a Chrome Extension that's passing messages. Every so often, when the content script calls
chrome.runtime.sendMessage({
message: 'hello',
});
it throws an error:
Uncaught Error: Extension context invalidated.
What does this error mean? I couldn't find any documentation on it.
It doesn't happen consistently. In fact, it's hard to reproduce. Seems to happen if I just leave the page open for a while in the background.
Another clue: I've written many Chrome Extensions with content scripts that pass messages and I haven't seen this error before. The main difference is that this content script is injected by the background page using
chrome.tabs.executeScript({
file: 'contentScript.js',
});
Does using executeScript instead of the manifest file somehow change the lifecycle of the content script?
This is certainly related to the message listener being lost in the middle of the connection between content and background scripts.
I've been using this approach in my extensions, so that I have a single module that I can use in both background and content scripts.
messenger.js
const context = (typeof browser.runtime.getBackgroundPage !== 'function') ? 'content' : 'background'
chrome.runtime.onConnect.addListener(function (port) {
port.onMessage.addListener(function (request) {
try {
const object = window.myGlobalModule[request.class]
object[request.action].apply(module, request.data)
} catch () {
console.error(error)
}
})
})
export function postMessage (request) {
if (context === 'content') {
const port = chrome.runtime.connect()
port.postMessage(request)
}
if (context === 'background') {
if (request.allTabs) {
chrome.tabs.query({}, (tabs) => {
for (let i = 0; i < tabs.length; ++i) {
const port = chrome.tabs.connect(tabs[i].id)
port.postMessage(request)
}
})
} else if (request.tabId) {
const port = chrome.tabs.connect(request.tabId)
port.postMessage(request)
} else if (request.tabDomain) {
const url = `*://*.${request.tabDomain}/*`
chrome.tabs.query({ url }, (tabs) => {
tabs.forEach((tab) => {
const port = chrome.tabs.connect(tab.id)
port.postMessage(request)
})
})
} else {
query({ active: true, currentWindow: true }, (tabs) => {
const port = chrome.tabs.connect(tabs[0].id)
port.postMessage(request)
})
}
}
}
export default { postMessage }
Now you'll just need to import this module in both content and background script. If you want to send a message, just do:
messenger.postMessage({
class: 'someClassInMyGlobalModuçe',
action: 'someMethodOfThatClass',
data: [] // any data type you want to send
})
You can specify if you want to send to allTabs: true, a specific domain tabDomain: 'google.com' or a single tab tabId: 12.

NodeJS VM2 proper way to access console when set to 'redirect'

I'm using the VM2 package to run user code. I'm trying to intercept console output and have set the NodeVM object's console property to 'redirect':
// Create a new sandbox VM for this request
const vm = new NodeVM( {
console: 'redirect',
timeout: 30000,
sandbox: { request, state, response },
require: {
external: true
}
});
According to the documentation that redirects console output to 'events'. I'm new to NodeJS, how do I hook into those events to capture the console.log messages executed inside the Sandbox?
After digging through the source code, I found this file where the event emit is occuring:
sandbox.js
if (vm.options.console === 'inherit') {
global.console = Contextify.readonly(host.console);
} else if (vm.options.console === 'redirect') {
global.console = {
log(...args) {
vm.emit('console.log', ...Decontextify.arguments(args));
return null;
},
info(...args) {
vm.emit('console.info', ...Decontextify.arguments(args));
return null;
},
warn(...args) {
vm.emit('console.warn', ...Decontextify.arguments(args));
return null;
},
error(...args) {
vm.emit('console.error', ...Decontextify.arguments(args));
return null;
},
dir(...args) {
vm.emit('console.dir', ...Decontextify.arguments(args));
return null;
},
time: () => {},
timeEnd: () => {},
trace(...args) {
vm.emit('console.trace', ...Decontextify.arguments(args));
return null;
}
};
}
All you need to do to listen to these events is to bind an event listener on the vm you've created:
// Create a new sandbox VM for this request
const vm = new NodeVM( {
console: 'redirect',
require: {
external: ['request']
}
});
vm.on('console.log', (data) => {
console.log(`VM stdout: ${data}`);
});
Likewise, you can bind to console.log, console.info, console.warn, console.error, console.dir, and console.trace. Hopefully this will save someone else some time.

Resources