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.
Related
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
I am trying to implement push notifications with react and nodejs using service workers.
I am having problem while i am showing notification to the user.
Here is my service worker code:
self.addEventListener('push', async (event) => {
const {
type,
title,
body,
data: { redirectUrl },
} = event.data.json()
if (type === 'NEW_MESSAGE') {
try {
// Get all opened windows that service worker controls.
event.waitUntil(
self.clients.matchAll().then((clients) => {
// Get windows matching the url of the message's coming address.
const filteredClients = clients.filter((client) => client.url.includes(redirectUrl))
// If user's not on the same window as the message's coming address or if it window exists but it's, hidden send notification.
if (
filteredClients.length === 0 ||
(filteredClients.length > 0 &&
filteredClients.every((client) => client.visibilityState === 'hidden'))
) {
self.registration.showNotification({
title,
options: { body },
})
}
}),
)
} catch (error) {
console.error('Error while fetching clients:', error.message)
}
}
})
self.addEventListener('notificationclick', (event) => {
event.notification.close()
console.log(event)
if (event.action === 'NEW_MESSAGE') {
event.waitUntil(
self.clients.matchAll().then((clients) => {
if (clients.openWindow) {
clients
.openWindow(event.notification.data.redirectUrl)
.then((client) => (client ? client.focus() : null))
}
}),
)
}
})
When new notification comes from backend with a type of 'NEW_MESSAGE', i get the right values out of e.data and try to use them on showNotification function but it seems like something is not working out properly because notification looks like this even though event.data equals to this => type = 'NEW_MESSAGE', title: 'New Message', body: , data: { redirectUrl: }
Here is how notification looks:
Thanks for your help in advance.
The problem was i assigned parameters in the wrong way.
It should've been like this:
self.registration.showNotification(title, { body })
I'm Kenyon Bowers.
I have some code that opens a open file dialog. It opens .DSCProj (which are specific to my project), and I am going to run some terminal commands in the directory that the opened file is in.
I have no idea how to do that.
preload.ts:
import { ipcRenderer, contextBridge } from "electron";
import { dialog } from '#electron/remote'
contextBridge.exposeInMainWorld("api", {
showOpenFileDialog: () => dialog.showOpenDialogSync({
properties: ["openFile"],
filters: [
{
name: "DSC Projects",
extensions: ["DSCProj"],
},
],
})
});
NewProject.ts:
declare var api: any;
function OpenProject(): void {
const file = api.showOpenFileDialog();
console.log("Done")
if(file != null){
localStorage.setItem('DirPath', file);
location.href='./views/projectOpen.html'
}
}
(() => {
document.querySelector('#btn-open-project')?.addEventListener('click', () => {
OpenProject();
}),
document.querySelector('#btn-new-project')?.addEventListener('click', () => {
location.href='./views/projectNew.html'
})
})()
As you can see on line 7, I'm setting local storage to the file's path. But I need to set it to the path of the directory that the file is in.
Whilst use of #electron/remote is great and all, you may be better served by implementing certain Electron modules within the main thread instead of the render thread(s). This is primarily for security but as a strong second reason, it keeps your code separated. IE: Separation of concerns.
Unlike vanilla Javascript, node.js has a simple function path.parse(path).dir to easily remove the file name (and extension) from the file path without needing to worry about which OS (IE: Directory separator) you are using. This would also be implemented within your main thread. Implementing something like this within your render thread would take a lot more work with vanilla Javascript to be OS proof.
Lastly, in the code below I will use a preload.js script that only deals with the movement of messages and their data between the main thread and render thread(s). I do not believe that the concrete implementation of functions in your preload.js script(s) is the right approach (though others may argue).
Note: I am not using typescript in the below code, but you should get the general idea.
Let's use the channel name getPath within the invoke method.
preload.js (main thread)
// Import the necessary Electron components.
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;
// White-listed channels.
const ipc = {
'render': {
// From render to main.
'send': [],
// From main to render.
'receive': [],
// From render to main and back again.
'sendReceive': [
'getPath'
]
}
};
// Exposed protected methods in the render process.
contextBridge.exposeInMainWorld(
// Allowed 'ipcRenderer' methods.
'ipcRender', {
// From render to main.
send: (channel, args) => {
let validChannels = ipc.render.send;
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, args);
}
},
// From main to render.
receive: (channel, listener) => {
let validChannels = ipc.render.receive;
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`.
ipcRenderer.on(channel, (event, ...args) => listener(...args));
}
},
// From render to main and back again.
invoke: (channel, args) => {
let validChannels = ipc.render.sendReceive;
if (validChannels.includes(channel)) {
return ipcRenderer.invoke(channel, args);
}
}
}
);
Now, in the main thread, let's listen for a call on the getPath channel. When called, open up the dialog and upon the return of a path, process it with Node's path.parse(path).dir function to remove the file name (and extension). Lastly, return the modified path.
main.js (main thread)
const electronBrowserWindow = require('electron').BrowserWindow;
const electronDialog = require('electron').dialog;
const electronIpcMain = require('electron').ipcMain;
const nodePath = require("path");
let window;
function createWindow() {
const window = new electronBrowserWindow({
x: 0,
y: 0,
width: 800,
height: 600,
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: nodePath.join(__dirname, 'preload.js')
}
});
window.loadFile('index.html')
.then(() => { window.show(); });
return window;
}
electronApp.on('ready', () => {
window = createWindow();
});
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// -----
// Let's listen for a call on the 'getPath' channel
electronIpcMain.handle('getPath', async () => {
// Dialog options.
const options = {
properties: ["openFile"],
filters: [
{
name: "DSC Projects",
extensions: ["DSCProj"],
}
]
}
// When available, return the modified path back to the render thread via IPC
return await openDialog(window, options)
.then((result) => {
// User cancelled the dialog
if (result.canceled === true) { return; }
// Modify and return the path
let path = result.filePaths[0];
let modifiedPath = nodePath.parse(path).dir; // Here's the magic.
console.log(modifiedPath); // Testing
return modifiedPath;
})
})
// Create an open dialog
function openDialog(parentWindow, options) {
return electronDialog.showOpenDialog(parentWindow, options)
.then((result) => { if (result) { return result; } })
.catch((error) => { console.error('Show open dialog error: ' + error); });
}
Here you will get the general idea about how to use the returned result.
index.html (render thread)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Open Project Test</title>
</head>
<body>
<input type="button" id="btn-open-project" value="Open Project">
</body>
<script>
document.getElementById('btn-open-project').addEventListener('click', () => {
openProject();
});
function openProject() {
window.ipcRender.invoke('getPath')
.then((path) => {
// As we are using "invoke" a response will be returned, even if undefined.
if (path === undefined) { return; } // When user cancels dialog.
console.log(path); // Testing.
// window.localStorage.setItem('DirPath', path);
// location.href='./views/projectOpen.html';
});
}
</script>
</html>
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"
]
}
],
...
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