Search code examples
javascripthtmlgoogle-chromegoogle-chrome-extensiongoogle-chrome-app

Array data lost in sendResponse from background script


I need to access data from my API from inside a contentscript, however since they are HTTP requests (and not HTTPS) they are blocked from inside a contentscript.

Due to that, I am doing all the requests from inside a background script, and trying to communicate between the backgroundscript and contentscript using the message API. Whenever I'm ready to use the data inside the contentscript, I send a message to the background script, which will then fetch the data from the API and send it as a response to contentscript. From the background script, if I console.log the data right before I send it, it's all good (an array of 4 positions). However, the received data in the contentscript is an empty array, all the data stored in the array is lost.

Here's the contentscript code snippet that sends the message:

if (typeof chrome.app.isInstalled !== 'undefined')
{
    console.log("gbdScreen sending requests")
    chrome.runtime.sendMessage({metric: "issues"}, function(response) 
    {
        setTimeout(function(){
            if (response !== undefined)
            {
                console.log(response)
                console.log(response.data)
            }
            else{
                console.log("gbdScreen-else")
                document.getElementById('gbdButton').click()
            }         
        }, 2000)
    })
}

And here's the background script, in which it receives the message, proceeds to fetch the data, and sends it back:

chrome.runtime.onMessage.addListener(function(request, sender, sendResponse) 
{
    let arr = []
    chrome.storage.sync.get('oauth2_token', function(res) 
    {
        if (res.oauth2_token != undefined)
        {
          chrome.tabs.query
          ({
              'active': true, 'lastFocusedWindow': true
          },
          function (tabs) 
          {
              let url = tabs[0].url.split("/")
              let owner = url[3]
              let repo = url[4].split("#")[0]
              let url_aux = `?owner=${owner}&repository=${repo}&token=${res.oauth2_token}`

              let url_fetch = url_base + '/commits' + url_aux

              // async function to make requests
              const asyncFetch = async () => await (await fetch(url_fetch)) 

              // commits request
              asyncFetch().then((resp) => resp.json()).then(function(data)
              {
                arr[0] = data
              }).catch(function(err)
              { 
                  console.log("Error: URL = " + url_fetch + "err: " + err)
              })

              // issues request
              url_fetch = url_base + '/issues' + url_aux
              asyncFetch().then((resp) => resp.json()).then(function(data)
              {
                arr[1] = data
              }).catch(function(err)
              { 
                  console.log("Error: URL = " + url_fetch + "err: " + err)
              })

              // branches request
              url_fetch = url_base + '/branches' + url_aux
              asyncFetch().then((resp) => resp.json()).then(function(data)
              {
                arr[2] = data
              }).catch(function(err)
              { 
                  console.log("Error: URL = " + url_fetch + "err: " + err)
              })

              // prs
              url_fetch = url_base + '/pullrequests' + url_aux
              asyncFetch().then((resp) => resp.json()).then(function(data)
              {
                arr[3] = data
              }).catch(function(err)
              { 
                  console.log("Error: URL = " + url_fetch + "err: " + err)
              })
              console.log(arr)
              sendResponse({data: arr}) // sends back to screen.js the data fetched from API
          })
        }
    })
    return true
})

I console.log inside both the backgroundscript and the contentscript, and it's all good in backgroundscript, but printing an empty array in contentscript. If anyone can shed some light, I know it's pretty messy code right now.


Solution

  • The question is basically a duplicate of Why is my variable unaltered after I modify it inside of a function? - Asynchronous code reference.

    You're populating the array asynchronously long time after you're sending the empty initial []. As for console.log, you see the final array only because devtools reads the variable on demand - when you expand the value, not when console.log runs.

    The solution is to use Promise.all to wait for all fetches to finish, and only then send the response.

    Let's simplify your code by using Mozilla's WebExtension Polyfill so that instead of return true with sendResponse we can return a Promise from onMessage listener, or even better use async/await:

    const FETCH_TYPES = [
      'commits',
      'issues',
      'branches',
      'pullrequests',
    ];
    
    async function fetchJson(type, aux) {
      const url = `${url_base}/${type}${aux}`;
      try {
        return (await fetch(url)).json();
      } catch (err) {
        console.log('Error: URL =', url, 'err:', err);
      }
    }
    
    browser.runtime.onMessage.addListener(async (request, sender) => {
      const {oauth2_token} = await browser.storage.sync.get('oauth2_token');
      if (oauth2_token) {
        const url = sender.tab.url.split('/');
        const owner = url[3];
        const repo = url[4].split('#')[0];
        const aux = `?owner=${owner}&repository=${repo}&token=${oauth2_token}`;
        const data = await Promise.all(FETCH_TYPES.map(type => fetchJson(type, aux)));
        return { data };
      }
    });
    

    P.S. Note how the code uses sender.tab.url as the message may arrive from an inactive tab.