sendToBackground in MAIN world doesn't work even with extensionId provided

Hello, thanks for the great framework! I've almost got my MVP done but have run into a brick wall with content scripts. Following the example on https://docs.plasmo.com/framework/messaging#message-flow, this snippet is what i'm after:
To send a message from a content script thats in the main world you'll have to include your extension's id in the request. Your extension's id can be found in chrome's extension manager window once you've built and added it to your browser.
Following the example above, here's my attempt:
import type { PlasmoCSConfig } from "plasmo"

import { sendToBackground } from "@plasmohq/messaging"

export const config: PlasmoCSConfig = {
matches: ["<all_urls>"],
run_at: "document_idle",
world: "MAIN"
}

window.sendTestResults = async (results) => {
console.log("sending test results", results)
await sendToBackground({
name: "results",
body: {
results
},
extensionId: "my_ext_id"
})
}
import type { PlasmoCSConfig } from "plasmo"

import { sendToBackground } from "@plasmohq/messaging"

export const config: PlasmoCSConfig = {
matches: ["<all_urls>"],
run_at: "document_idle",
world: "MAIN"
}

window.sendTestResults = async (results) => {
console.log("sending test results", results)
await sendToBackground({
name: "results",
body: {
results
},
extensionId: "my_ext_id"
})
}
I have a background/messages/results.ts ready and listening, and i've confirmed that this sendTestResults function IS getting called by my executeScript call in background/index.ts. (Basically, i'm triggering a script in the window context to test for accessibility issues, and i'm called window.sendTestResults in its callback. The function is being triggered because i'm seeing console logs in it!) I keep getting an error: Uncaught (in promise) Error: Extension runtime is not available. I've tried this approach, ports, AND relays, but nothing seems to work in the MAIN world. Does anyone have a working example of a content script in the MAIN world triggering a message in the background script? Or at least a strong recommendation for the Plasmo way to send the results of a function called on the window of the page to my background script? Thanks in advance for the help, i'm at the end of my rope!
Plasmo Docs
Messaging API – Plasmo
The Plasmo messaging API is a powerful tool for sending real-time messages between different parts of your extension.
50 Replies
RandomJay
RandomJayOP6d ago
For some added detail, I've also tried bypassing plasmo messaging with the native APIs by adding "chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) " but i get a different error in the console when i put it in my content.ts file:
Uncaught TypeError: Cannot read properties of undefined (reading 'onMessage')
at 5tZoc.@parcel/transformer-js/src/esmodule-helpers.js
Uncaught TypeError: Cannot read properties of undefined (reading 'onMessage')
at 5tZoc.@parcel/transformer-js/src/esmodule-helpers.js
filthytone
filthytone5d ago
main world won't be able to access extension apis ... you pretty much would need to use window.postMessage and have a content script listen for those messages and foward them off to the background
RandomJay
RandomJayOP5d ago
Thanks @filthytone , so as long as i fire a postMessage from the content script you're saying the background should be able to pick it up? I'll give that a shot today, thank you! Should I file an issue on the repo to get that example removed from the docs since it doesn't sound like it'd work? Here's teh example: contents/componentInTheMainWorld.tsx:
import { sendToBackground } from "@plasmohq/messaging"
import type { PlasmoCSConfig } from "plasmo"

export const config: PlasmoCSConfig = {
matches: ["<all_urls>"],
world: "MAIN"
}
...
const resp = await sendToBackground({
name: "ping",
body: {
id: 123
},
extensionId: 'llljfehhnoeipgngggpomjapaakbkyyy' // find this in chrome's extension manager
})

console.log(resp)
import { sendToBackground } from "@plasmohq/messaging"
import type { PlasmoCSConfig } from "plasmo"

export const config: PlasmoCSConfig = {
matches: ["<all_urls>"],
world: "MAIN"
}
...
const resp = await sendToBackground({
name: "ping",
body: {
id: 123
},
extensionId: 'llljfehhnoeipgngggpomjapaakbkyyy' // find this in chrome's extension manager
})

console.log(resp)
YAGPDB.xyz
YAGPDB.xyz5d ago
Gave +1 Rep to @filthytone (current: #4 - 22)
Arcane
Arcane5d ago
@RandomJay has reached level 1. GG!
RandomJay
RandomJayOP5d ago
I thought sendToBackground was a wrapper for postMessage but apparently not?
filthytone
filthytone5d ago
it ends up calling chrome.runtime.sendMessage(extensionId, blah) that might work actually .. what's the error you get
RandomJay
RandomJayOP5d ago
Uncaught (in promise) Error: Extension runtime is not available. So i think you're right that the extension runtime isn't available in the main world script, i thought there was some kind of parcel magic that was injecting a polyfill into the content script or something, but i'll admit i didn't go too deep into Plasmo's source code to confirm
filthytone
filthytone5d ago
Chrome for Developers
externally_connectable  |  Chrome Extensions  |  Chrome for Develop...
Reference documentation for the externally_connectable property of manifest.json.
filthytone
filthytone5d ago
bc technically it's in a web page context now it might need that whitelisted just keep in mind all messaging is basically untrusted from main world
RandomJay
RandomJayOP5d ago
one sec, i'll try that right now Well, the good news is that the error is gone! Not receiving the message in the background script though, just double checking that everything is named properly
filthytone
filthytone5d ago
Yea so now it’s an external message So instead of /messaging/whatever.ts You’d have /messaging/external/whatever.ts
RandomJay
RandomJayOP5d ago
oh! trying that one sec
filthytone
filthytone5d ago
And again, anything that comes into that message handler is untrusted So you have to do type checks , sender checks etc
RandomJay
RandomJayOP5d ago
Even if i explicitly defined my extension id in the manifest?
filthytone
filthytone5d ago
That might be ok but you’ll want to test
RandomJay
RandomJayOP5d ago
btw, that worked! I was able to receive the message via the external route, thank you for the advice there. For anyone finding this later, the correct path was messages/external/handler.ts
filthytone
filthytone5d ago
What did you put for the full value
RandomJay
RandomJayOP5d ago
of the manifest? one sec
Arcane
Arcane5d ago
@RandomJay has reached level 2. GG!
RandomJay
RandomJayOP5d ago
"externally_connectable": {
"ids": [
"kel...ehg"
],
"matches": [
"http://*/*",
"https://*/*"
]
},
"externally_connectable": {
"ids": [
"kel...ehg"
],
"matches": [
"http://*/*",
"https://*/*"
]
},
filthytone
filthytone5d ago
Yea so that’s like an or statement You can test by visiting a page , open devtools, and do a chrome.runtime.sendMessage call If you see it then it’s exposed to any site
RandomJay
RandomJayOP5d ago
Got it, so to be safe i should pass along some kind of payload for verification when i trigger the extension manually. Basically, my extension runs an automated accessibility test script against any webpage, and then reports back the findings via this external script so that I can process and highlight the issues in a dev-friendly way
filthytone
filthytone5d ago
Yea so if you have to be in main, you have to assume untrusted .. so any validation is good
RandomJay
RandomJayOP5d ago
so if i generate an id from the backend when the test is kicked off to ensure that the call is legitimate when i run the processing, that might be a good way to validate that the request is legit and at least make sure it's attributed to the right user. I already have auth enabled here, so i can probably use that too
filthytone
filthytone5d ago
Another way is use the scripting api to initiate the injection of code from background but yea anything you can do to validate the incoming data will be important
RandomJay
RandomJayOP5d ago
I actually started with that, but unfortunately there's no way to report the results BACK to the background without being in main unfortunately. I tried passing a callback along, but since functions aren't serializable I can't actually get the results back. Unless there's an easy way to do that that you know of? I'd much prefer that approach if there's a way basically I do this:
// Inject the axe script into the active tab
await chrome.scripting.executeScript({
target: {
tabId
},
files: ["axe.min.js"],
world: "MAIN"
})
// Configure axe
await chrome.scripting.executeScript({
target: {
tabId
},
func: configureAxe,
world: "MAIN"
})
// Run the test
await chrome.scripting.executeScript({
target: {
tabId
},
func: runA11yAudit,
world: "MAIN"
})
// Inject the axe script into the active tab
await chrome.scripting.executeScript({
target: {
tabId
},
files: ["axe.min.js"],
world: "MAIN"
})
// Configure axe
await chrome.scripting.executeScript({
target: {
tabId
},
func: configureAxe,
world: "MAIN"
})
// Run the test
await chrome.scripting.executeScript({
target: {
tabId
},
func: runA11yAudit,
world: "MAIN"
})
but that last one is the kicker, the response only returns a promise with the inject result, not the result of the function itself
filthytone
filthytone5d ago
If you inject a function , whatever it returns will be the result of the execute script call
Sam
Sam5d ago
Have you tried sendToBackgroundViaRelay? That is how I do it, sorry for jumping in so late.
RandomJay
RandomJayOP5d ago
I did, that resulted in a different error for me, unfortunately. Are you kidding?! i appreciate any and all help!
Sam
Sam5d ago
I didn't want to discount any of the work and discussion you've done so far.
RandomJay
RandomJayOP5d ago
Knowing that I have the external script to fall back on, i'm happy to experiment with that too, let me switch over to relay and i'll tell you about that error. Does relay also require the externally_connectable flag in the manifest to be set?
Sam
Sam5d ago
I don't believe so. I do have my extension configured with host_permissions. Not sure if that is even needed though. To get the relay working though I had to register it... 1 sec I'm getting the code.
RandomJay
RandomJayOP5d ago
While we're playing with that, MASSIVE thank you, @filthytone , you really helped me out here
YAGPDB.xyz
YAGPDB.xyz5d ago
Gave +1 Rep to @filthytone (current: #4 - 23)
Sam
Sam5d ago
OK so in my example I have a file at src/background/messages/export.ts. To use that message from a MAIN world content script I "register" it in an isolated world script. src/contents/main-world-handlers.ts
import type { PlasmoCSConfig } from 'plasmo';

import { relayMessage } from '@plasmohq/messaging';

export const config: PlasmoCSConfig = {
all_frames: true,
run_at: 'document_start',
};

relayMessage({
name: 'export',
body: {},
});
import type { PlasmoCSConfig } from 'plasmo';

import { relayMessage } from '@plasmohq/messaging';

export const config: PlasmoCSConfig = {
all_frames: true,
run_at: 'document_start',
};

relayMessage({
name: 'export',
body: {},
});
Then inside my MAIN work script I can call it with
import { sendToBackgroundViaRelay } from '@plasmohq/messaging';
...
void sendToBackgroundViaRelay<ExportRequest>({
name: 'export',
body: {
action: 'imageData',
payload: {
id,
imageMetadataMap,
},
},
});
import { sendToBackgroundViaRelay } from '@plasmohq/messaging';
...
void sendToBackgroundViaRelay<ExportRequest>({
name: 'export',
body: {
action: 'imageData',
payload: {
id,
imageMetadataMap,
},
},
});
filthytone
filthytone5d ago
Yea that’s what I was saying early on is you have to have a isolated content script proxy it
Sam
Sam5d ago
Yeah @filthytone has a ton of knowledge around core extension development.
filthytone
filthytone5d ago
This is also untrusted data BUT this approach is better bc it works in Firefox too Or use the scripting API .. but all of these are viable options depending on what you need to do
RandomJay
RandomJayOP5d ago
Thank you @Sam, this is a great example. I'll play with this and see if i can get it working. I'm 99% of the way there already. and @filthytone , the scripting API is my favorite approach becuase it's so simple, I don't really need to inject more than the window.axe object to run tests against. Maybe it's just the way i'm running the test that's preventing me from getting the results from the execution back, i'm probably stuck in a closure or something so i'll start there and then work my way back to the less trusted but more flexible options 👍
YAGPDB.xyz
YAGPDB.xyz5d ago
Gave +1 Rep to @Sam (current: #11 - 4)
filthytone
filthytone5d ago
sounds good .. i use it in production to do something similar where it returns me results and it def works i think the result of executeScript is a Promise<Array<any>> so you just have to iterate it to get the results
RandomJay
RandomJayOP5d ago
I'll def take a look. Here's the function at its most basic:
await window.axe.run(
{
allowedOrigins: ["<unsafe_all_origins>"],
exclude: []
},
async (err, results) => {
if (err) {
console.log(err)
}

return results
}
)
await window.axe.run(
{
allowedOrigins: ["<unsafe_all_origins>"],
exclude: []
},
async (err, results) => {
if (err) {
console.log(err)
}

return results
}
)
I think it's that the results only exist in the callback function, so i need to see if i can hoist it outta there 🙂
filthytone
filthytone5d ago
i don't know that api, but i'd play w/ returning a Promise that resolves
RandomJay
RandomJayOP5d ago
Dang, you're 100% right, I was overcomplicating this by a lot. All I needed to do was:
const results = await new Promise((resolve, reject) => {
window.axe.run(
{
allowedOrigins: ["<unsafe_all_origins>"],
exclude: []
},
(err, results) => {
if (err) {
console.log(err)
reject(err)
} else {
resolve(results)
}
}
)
})
const results = await new Promise((resolve, reject) => {
window.axe.run(
{
allowedOrigins: ["<unsafe_all_origins>"],
exclude: []
},
(err, results) => {
if (err) {
console.log(err)
reject(err)
} else {
resolve(results)
}
}
)
})
And it works swimingly, all results displayed in the background script directly 🤦‍♂️ Thank you, ended up being a newbie question after all
filthytone
filthytone5d ago
🤘
RandomJay
RandomJayOP5d ago
Thank you again @filthytone and @Sam , I learned a lot here about some pretty esoteric things. Appreciate you both taking the time!
YAGPDB.xyz
YAGPDB.xyz5d ago
Gave +1 Rep to @filthytone (current: #4 - 24)
filthytone
filthytone5d ago
Get rid of your externally connectable too if you’re gonna go with scripting Then you’re back to safe
RandomJay
RandomJayOP5d ago
Definitely, thank you. Much cleaner this way
Want results from more Discord servers?
Add your server