Search code examples
javascriptgoogle-chrome-extensionchrome-extension-manifest-v3

ManifestV3 new promise error: The message port closed before a response was received


I'm working on an extension that does a lot of message-passing between its content scripts and the background service worker (manifest V3), and I've noticed an odd issue with the new Promise-based V3 APIs, specifically with the sendResponse() function.

With API calls that expect a response, everything works fine. But if I don't need a response and don't provide a callback function or use the promise's .then() method (or async/await), a Promise error is thrown - it says "The message port closed before a response was received."

Strangely, the call still works, so I guess this error is more like a warning.

Code example:

In a content script, sending a message to the background:

chrome.runtime.sendMessage({ type: 'toggle_setting' })

The background script gets the message and does something, then exits without sending a response:

chrome.runtime.onMessage.addListener( (message, sender, sendResponse) => {
  if (message.type === 'toggle-setting') {
    //* do whatever it does
  }
})

That background code is what throws the error described above. But if I add one line to it and call the sendResponse() function with no parameters, no error happens.

chrome.runtime.onMessage.addListener( (message, sender, sendResponse) => {
  sendResponse()
  if (message.type === 'toggle-setting') {
    //* do whatever it does
  }
})

So this gets rid of the error messages, but I'm not really clear on why it's necessary when there is no response needed or expected. Is there some other way to signal that to the Promise-based V3 APIs, or is it now necessary to call sendResponse() even if you don't need to?


Solution

  • It's a bug in Chrome 99-101, fixed in Chrome 102.

    The reason is that sendMessage is now promisified internally, so we can await it, but the byproduct is that when we don't specify a callback ourselves, it is added internally anyway in order for the call to return a Promise, which means that since we don't call sendResponse in onMessage, the API will think that it was us who made a mistake of using a callback and not providing a response, and report it as such.

    Workaround 1: call sendResponse() inside chrome.runtime.onMessage

    • sender (in the question it's the content script):

      chrome.runtime.sendMessage('test');
      
    • receiver (in the question it's the background script):

      chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
        doSomethingWithoutResponding(msg);
        sendResponse();
      });
      

    Workaround 2: suppress this specific error

    Also patching the API's inherent lack of a callstack prior to the call.

    // sender (in the question it's the content script)
    sendMessage('foo');
    
    // sendMessage('foo').then(res => whatever(res));
    // await sendMessage('foo');
    
    function sendMessage(msg) {
      const err1 = new Error('Callstack before sendMessage:');
      return new Promise((resolve, reject) => {
        chrome.runtime.sendMessage(msg, res => {
          let err2 = chrome.runtime.lastError;
          if (!err2 || err2.message.startsWith('The message port closed before')) {
            resolve(res);
          } else {
            err2 = new Error(err2.message);
            err2.stack += err1.stack.replace(/^Error:\s*/, '');
            reject(err2);
          }
        });
      });
    }