Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

The behavior of browser.runtime.onMessage #1560

Closed
massongit opened this issue Apr 5, 2025 · 2 comments
Closed

The behavior of browser.runtime.onMessage #1560

massongit opened this issue Apr 5, 2025 · 2 comments

Comments

@massongit
Copy link

massongit commented Apr 5, 2025

Feature Request

If we do not use webextension-polyfill, when browser.runtime.onMessage in a content script receives a message from a popup page etc. and returns a response, a Chrome extension needs to use a listener function's argument sendResponse.
However, a Firefox extension needs to return the response as the return value of the listener function.

// entrypoints/content.ts
export default defineContentScript({
  matches: ["<all_urls>"],
  main() {
    browser.runtime.onMessage.addListener(async (message, _, sendResponse) => {
      const response = message + "2";

      // Chrome and Firefox have different methods
      switch (import.meta.env.BROWSER) {
        case "chrome":
          sendResponse(response);
          break;
        case "firefox":
          return response;
      }
    });
  },
});
// entrypoints/popup/App.tsx
import React, { useState } from "react";
import "./App.css";

async function getFromContentScript(
  setResponse: React.Dispatch<React.SetStateAction<string>>,
) {
  const tabs = await browser.tabs.query({ active: true, currentWindow: true });

  if (tabs.length === 0) {
    return;
  }

  const tabID = tabs[0].id;

  if (tabID !== undefined) {
    setResponse(await browser.tabs.sendMessage(tabID, "test1"));
  }
}

function App() {
  const [response, setResponse] = useState("");
  getFromContentScript(setResponse);
  return <div>{response}</div>;
}

export default App;

Accoding to Upgrade Guide, in in v0.20.0, webextension-polyfill is removed.
If so, I would like the way responses are returned to be unified between Chrome and Firefox extensions.

Is your feature request related to a bug?

N/A

What are the alternatives?

  • You describe in WXT's documentation such as Upgrade Guide that we need to use webextension-polyfill if our extensions use browser.runtime.onMessage

Additional context

@aklinker1
Copy link
Collaborator

aklinker1 commented Apr 6, 2025

This is not how messaging works. I'll clear up a couple of misconnecptions here:

  1. In all contexts (background vs content script vs html pages), browser.runtime.onMessage behaves the same way
  2. You need to be very careful when making the callback an async function
  3. Never use an async function and sendResponse at the same time - you'll get undefined behavior, probably like you're experiencing

To return a response, you have two options:

  1. Return a promise of the response
  2. Return true and use sendResponse at a later point.

Keep in mind that as soon as you make the callback async, the function ALWAYS RETURNS A PROMISE. If you don't return a value explicitly, the function will return a promise of undefined and send a response of undefined back to the sender.

Let's cleanup your code. Personally, I prefer to use promises in my message handlers, but I'll show how to do both.

async function someAsyncWork(): Promise<string> {
  return message + "2";
};

browser.runtime.onMessage.addListener((message) => {
  return someAsyncWork();
});

Note that my onMessage callback is not async! I never make the callback async to avoid accidentally returning undefined when I use if statements to handle multiple types of messages. In this case, there's only one message, so you could make it async and await the promise, but that will break as you scale up and add more types of messages that this listener receives, or if you add additional onMessage listeners.

Or using sendResponse:

function someAsyncWorkWithCallback(cb: () => void): void {
  setTimeout(cb, 1000);
}

browser.runtime.onMessage.addListener((message, _, sendResponse) => {
  someAsyncWorkWithCallback(() => {
    sendResponse(message + "2")
  });
  return true
});

Or in your case, if we return a value immediately, you still need to either: return a promise, or return true and call sendResponse after the function has returned. In this case, it's easiest to just use Promise.resolve:

browser.runtime.onMessage.addListener((message) => {
  return Promise.resolve(message + "2");
});

This is the correct way to write message handlers. It works on all active browser and manifest combinations (Firefox MV2, Firefox MV3, Chrome MV3, Safari MV2, Safari MV3).

It does not work on Chrome MV2 (doesn't support promises, only sendResponse), but that doesn't matter since it's dead now. Which is why it was OK for WXT to drop the polyfill.

@massongit
Copy link
Author

massongit commented Apr 6, 2025

Thank you for your reply!

Or using sendResponse:

Based on the above, I successfully return a response using the following method:

// entrypoints/content.ts
async function someAsyncWork(message: string, sendResponse: (response?: any) => void) {
  sendResponse(message + "2");
}

export default defineContentScript({
  matches: ["<all_urls>"],
  main() {
    browser.runtime.onMessage.addListener((message, _, sendResponse: (response?: any) => void) => {
      someAsyncWork(message,sendResponse);
      return true;
    });
  },
});

Now that I know we can return a response without webextension-polyfill, I close this issue.

@massongit massongit closed this as not planned Won't fix, can't repro, duplicate, stale Apr 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants