Thordax Thordax - 14 days ago 8
Javascript Question

Promise from browser.runtime.sendMessage() fulfilling prior to asynchronous call to sendResponse()

I have made a Firefox WebExtension that interacts with an executable (using

).

Here is the manifest of the extension:

// manifest.json file

{
"name": "My Extension",
"short_name": "myext",
"version": "2.7",
"manifest_version": 2,
"background": {
"scripts": ["background-script.js"],
"persistent": true
},
"externally_connectable": {
"ids": ["*"],
"http://localhost/*"]
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content-script.js"]
}
],
"applications": {
"gecko": {
"id": "myext@myext.mysite.com",
"strict_min_version": "50.0.0"
}
},
"icons": {
"16": "icon-16.png",
"32": "icon-32.png",
"128": "icon-128.png"
},
"permissions": [
"nativeMessaging",
"webRequest"
]
}


Here is the content script:

// content-script.js

window.addEventListener("message", function(event) {
if (event.source == window &&
event.data.direction &&
event.data.direction == "from-page-script") {
alert("Content script received message: \"" + JSON.stringify(event.data.message) + "\"");

var sending = browser.runtime.sendMessage(event.data.message);

sending.then(handleResponse, handleError);

}
});

function handleResponse(message)
{
console.log("content-script : handleResponse : message = " + JSON.stringify(message));
window.wrappedJSObject.foo.p = "OK : " + JSON.stringify(message);
}

function handleError(error) {
console.log("Message from the background script : " + error.message);
window.wrappedJSObject.foo.p = "error";
}


And here is my background script:

// background-script.js

var port;

browser.runtime.onMessage.addListener(function(request, sender, sendResponse)
{
port = browser.runtime.connectNative("ping_pong");
var result = port.postMessage(request);

port.onMessage.addListener(function(response)
{
console.log("Received: " + JSON.stringify(response));
sendResponse(response);
});
});


I have a problem regarding the following line in the background script:

sendResponse(response);


The method
handleResponse()
of the content script is called before the
sendResponse()
of the background script.

So, when the executable takes a longer time to do an action, the result of the executable is not sent to the content script, and the content script receives an undefined result.

Is there another way to send the result of the executable from the background script to the content script? Or use the callback another way?

Answer

The use of Promises is causing some confusion here. Your handleResponse() is being called with the Promise fulfilled with no arguments because you exit the background script's runtime.onMessage listener without calling sendResponse(). If sendResponse() had been called, then the argument would be the message1. Thus, you will need to adjust your .then to differentiate between receiving no arguments (sendResponse() not called) and an argument containing the response message (sendResponse() called). If I recall correctly, if you were using chrome.runtime.sendMessage(), your responseCallback function would be called unless sendResponse() is called.

MDN describes it as:

If the sender sent a response, this will be fulfilled with the response as a JSON object. Otherwise it will be fulfilled with no arguments. If an error occurs while connecting to the extension, the promise will be rejected with an error message.

To call sendResponse() asynchronously, you need to return true; from your runtime.onMessage listener

You are calling sendResponse() from within an asynchronous callback. As a result, you exit the runtime.onMessage listener prior to sendResponse() being called. If you are going to do that, and still want to call sendResponse(), you need to return true from your runtime.onMessage listener.

From MDN runtime.onMessage regarding sendResponse():

This function returns a boolean. It should return true from the event listener if you wish to call sendResponse after the event listener returns.

So your code could be:

browser.runtime.onMessage.addListener(function(request, sender, sendResponse) 
{
    port = browser.runtime.connectNative("ping_pong");
    var result = port.postMessage(request);

    port.onMessage.addListener(function portOnMessageListener(response)
    {
        //sendResponse is only valid once. So, if this is how you want to use it, you need
        //  to remove the listener, or in some other way not call sendResponse twice.
        port.onMessage.removeListener(portOnMessageListener);
        console.log("Received: " + JSON.stringify(response));
        sendResponse(response);
    });
    return true;
});

sendResponse() is valid only once

The sendResponse() function is valid for only one message (i.e. you get to send only one response per message sent). If your desire is to send multiple messages to the content script, you will need to set up doing so in a different way.


  1. The MDN docs state the message is a "JSON object", but probably mean that it is an Object that was converted from the JSON used to represent it in the message)