30

I need to define my Service Worker as persistent in my Chrome extension because I'm using the webRequest API to intercept some data passed in a form for a specific request, but I don't know how I can do that. I've tried everything, but my Service Worker keeps unloading.

How can I keep it loaded and waiting until the request is intercepted?

Keven Augusto
  • 305
  • 1
  • 3
  • 4

6 Answers6

55

This is caused by these problems in ManifestV3:

  • crbug.com/1024211, the worker doesn't wake up for webRequest events.
    The workarounds are listed below.

  • crbug.com/1271154, the worker is randomly broken after an update.
    Mostly fixed in Chrome 101.

  • Per the service worker (SW) specification, it can't be persistent and the browser must forcibly terminate all of SW connections such as network requests or ports after some time, which in Chrome is 5 minutes. Chromium team currently considers this behavior intentional and good for extensions, because the team never investigated real world situations where this SW behavior is bad in case an extension has to observe frequent events:

    • chrome.tabs.onUpdated/onActivated,
    • chrome.webNavigation if not scoped to a rare url,
    • chrome.webRequest if not scoped to a rare url or type,
    • chrome.runtime.onMessage/onConnect for messages from content script in all tabs.

    Such events are generated in response to user actions, so there are natural pauses for a few minutes, during which the SW is terminated. Then it starts again for a new event, which takes at least 50ms to create the process plus the time to load and compile your code, ~50ms on the average, i.e. it's ~100 times heavier than ~1ms needed to call a simple JS event listener. For an active online user it may restart hundreds of times a day thus wearing down CPU/disk/battery and often introducing a frequent perceivable lag of the extension's reaction.

Workarounds

For webRequest not waking up

Additionally subscribe to an API like chrome.webNavigation as shown in the other answer(s).

This applies to extensions that observe infrequent events, e.g. you specified urls filter for webRequest/webNavigation for just one rarely visited site. Such extensions can be reworked to avoid the need for a persistent background script so it will start only a few times a day, which will be good for memory footprint while not stressing CPU too much. You would save/load the variables/state in each listener via chrome.storage.session (temporary, 1MB max), or chrome.storage.local, or even IndexedDB that's much faster for big/complex data.

But if you MUST observe frequent events (listed in the beginning of this answer), you'll have to prolong the background script's lifetime artificially using the following workarounds.

"Persistent" service worker while a connectable tab is present

In case you don't use ports with a content script that runs in all tabs (shown in another workaround below), here's an example of opening a runtime port from any tab's content script or from another page of the extension like the popup page and reconnecting it before 5 minutes elapse.

Downsides:

  • The need for an open web page tab or an open extension tab/popup.
  • Broad host permissions (like <all_urls> or *://*/*) for content scripts which puts most extensions into the slow review queue in the web store.

Warning! Also implement the workaround for sendMessage (below) if you use sendMessage.

Warning! You don't need this if you already use the workaround for chrome.runtime.connect (below) with a content script that runs in all tabs.

  • manifest.json, the relevant part:

      "permissions": ["scripting"],
      "host_permissions": ["<all_urls>"],
      "background": {"service_worker": "bg.js"}
    
    
  • background service worker bg.js:

    let lifeline;
    
    keepAlive();
    
    chrome.runtime.onConnect.addListener(port => {
      if (port.name === 'keepAlive') {
        lifeline = port;
        setTimeout(keepAliveForced, 295e3); // 5 minutes minus 5 seconds
        port.onDisconnect.addListener(keepAliveForced);
      }
    });
    
    function keepAliveForced() {
      lifeline?.disconnect();
      lifeline = null;
      keepAlive();
    }
    
    async function keepAlive() {
      if (lifeline) return;
      for (const tab of await chrome.tabs.query({ url: '*://*/*' })) {
        try {
          await chrome.scripting.executeScript({
            target: { tabId: tab.id },
            function: () => chrome.runtime.connect({ name: 'keepAlive' }),
            // `function` will become `func` in Chrome 93+
          });
          chrome.tabs.onUpdated.removeListener(retryOnTabUpdate);
          return;
        } catch (e) {}
      }
      chrome.tabs.onUpdated.addListener(retryOnTabUpdate);
    }
    
    async function retryOnTabUpdate(tabId, info, tab) {
      if (info.url && /^(file|https?):/.test(info.url)) {
        keepAlive();
      }
    }
    

If you also use sendMessage

Always call sendResponse() in your chrome.runtime.onMessage listener even if you don't need the response. This is a bug in MV3. Also, make sure you do it in less than 5 minutes time, otherwise call sendResponse immediately and send a new message back via chrome.tabs.sendMessage (to the tab) or chrome.runtime.sendMessage (to the popup) after the work is done.

If you already use ports e.g. chrome.runtime.connect

Reconnect each port before 5 minutes elapse.

  • background script example:

    chrome.runtime.onConnect.addListener(port => {
      if (port.name !== 'foo') return;
      port.onMessage.addListener(onMessage);
      port.onDisconnect.addListener(deleteTimer);
      port._timer = setTimeout(forceReconnect, 250e3, port);
    });
    function onMessage(msg, port) {
      console.log('received', msg, 'from', port.sender);
    }
    function forceReconnect(port) {
      deleteTimer(port);
      port.disconnect();
    }
    function deleteTimer(port) {
      if (port._timer) {
        clearTimeout(port._timer);
        delete port._timer;
      }
    }
    
  • client script example e.g. a content script:

    let port;
    function connect() {
      port = chrome.runtime.connect({name: 'foo'});
      port.onDisconnect.addListener(connect);
      port.onMessage.addListener(msg => {
        console.log('received', msg, 'from bg');
      });
    }
    connect();
    

"Forever", via a dedicated tab, while the tab is open

Open a new tab with an extension page inside e.g. chrome.tabs.create({url: 'bg.html'}).

It'll have the same abilities as the persistent background page of ManifestV2 but a) it's visible and b) not accessible via chrome.extension.getBackgroundPage (which can be replaced with chrome.extension.getViews).

Downsides:

  • consumes more memory,
  • wastes space in the tab strip,
  • distracts the user,
  • when multiple extensions open such a tab, the downsides snowball and become a real PITA.

You can make it a little more bearable for your users by adding info/logs/charts/dashboard to the page and also add a beforeunload listener to prevent the tab from being accidentally closed.

Future of ManifestV3

Let's hope Chromium will provide an API to control this behavior without the need to resort to such dirty hacks and pathetic workarounds. Meanwhile describe your use case in crbug.com/1152255 if it isn't already described there to help Chromium team become aware of the established fact that many extensions may need a persistent background script for an arbitrary duration of time and that at least one such extension may be installed by the majority of extension users.

wOxxOm
  • 53,493
  • 8
  • 111
  • 119
  • Thank you very much!! I spent two days thinking it was an error in my code, I just switched to MV2 and now it's working!! – Keven Augusto Mar 14 '21 at 18:03
  • So folks are aware in the "Keep alive forever via runtime ports" option the `executeScript` invocation's `ScriptInjection` requires a `function` key and NOT a `func` key (Version 90.0.4430.212). This contradicts the [ScriptInjection](https://developer.chrome.com/docs/extensions/reference/scripting/#type-ScriptInjection) documentation but does match the [scripting documentation](https://developer.chrome.com/docs/extensions/reference/scripting/#runtime-functions) – Poke Jul 09 '21 at 17:17
  • none of these approaches do what the answer purports. the `keepAlive` strategy is just redundancy and just who in their right minds would want to keep a tab open? this answer stands downvoted for these reasons. – even0 Sep 30 '21 at 07:28
  • 3
    @surajsharma, everything in this answer was tested by multiple users and does exactly what it says. – wOxxOm Sep 30 '21 at 08:48
  • @wOxxOm I see you all over regarding the persistent v3 service worker. I am having an issue I am having trouble fixing. I'm using your script. The keep alive functions maybe 3 or 4 times then my background service worker restarts itself. I have a file like `console.log("start")`, then all ur keep alive stuff (added logs), then my actual backgroundController logic. I will get a log that the timeout is running and keepAlive port is reconnecting. This happens maybe 3 or 4 times. Then a log `start` and a run-through of all my background app setup again, indicating a restart of the service worker. – diabetesjones Feb 23 '22 at 03:29
  • Continued: is there anything you can think of that would be causing this to run successfully a few times over 15 to 20 mins then collapse? It's very important to my extension that the bg controller does not restart randomly, it holds state and such, and communicates via the content script. – diabetesjones Feb 23 '22 at 03:31
  • Please don't use comments to ask questions. Post a new question with your real actual code ([MCVE](/help/mcve)). – wOxxOm Feb 23 '22 at 05:39
  • Thanks for your answer! While you have correctly mentioned "Persistent service worker while a **connectable** tab is present", I feel you could expand on that in the "Downsides" section of your answer also. This is because at first I thought the approach works as long as the browser window has _any_ website tabs open. But it does not work in the specific case where the browser has _only_ non-connectable tabs open, such as CWS pages or the new tab page. So, if a user opens example.com, then navigates to new tab, and then goes to google.com after some time, the SW will have restarted. – Gaurang Tandon Jun 01 '22 at 08:26
5

unlike the chrome.webRequest API the chrome.webNavigation API works perfectly because the chrome.webNavigation API can wake up the service worker, for now you can try putting the chrome.webRequest API api inside the chrome.webNavigation.

chrome.webNavigation.onBeforeNavigate.addListener(function(){

   chrome.webRequest.onResponseStarted.addListener(function(details){

      //.............
      
      //.............

   },{urls: ["*://domain/*"],types: ["main_frame"]});


},{
    url: [{hostContains:"domain"}]
});
4

If i understand correct you can wake up service worker (background.js) by alerts. Look at below example:

  1. manifest v3
"permissions": [
    "alarms"
],
  1. service worker background.js:
chrome.alarms.create({ periodInMinutes: 4.9 })
chrome.alarms.onAlarm.addListener(() => {
  console.log('log for debug')
});

Unfortunately this is not my problem and may be you have different problem too. When i refresh dev extension or stop and run prod extension some time service worker die at all. When i close and open browser worker doesn't run and any listeners inside worker doesn't run it too. It tried register worker manually. Fore example:

// override.html
<!DOCTYPE html>
<html lang="en">

  <head>...<head>
  <body>
    ...
    <script defer src="override.js"></script>
  <body>
<html>
// override.js - this code is running in new tab page
navigator.serviceWorker.getRegistrations().then((res) => {
  for (let worker of res) {
    console.log(worker)
    if (worker.active.scriptURL.includes('background.js')) {
      return
    }
  }

  navigator.serviceWorker
    .register(chrome.runtime.getURL('background.js'))
    .then((registration) => {
      console.log('Service worker success:', registration)
    }).catch((error) => {
      console.log('Error service:', error)
    })
})

This solution partially helped me but it does not matter because i have to register worker on different tabs. May be somebody know decision. I will pleasure.

  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Neeraj Nov 17 '21 at 11:47
  • 1
    Technically, this is answer is not related to persistence of the background script but it still provides a workaround for an inherent bug in ManifestV3 where the background script gets lost altogether during an update, https://crbug.com/1271154. – wOxxOm Nov 26 '21 at 07:26
2

As Clairzil Bawon samdi's answer that chrome.webNavigation could wake up the service worker in MV3, here are workaround in my case:

// manifest.json
...
"background": {
  "service_worker": "background.js"
},
"host_permissions": ["https://example.com/api/*"],
"permissions": ["webRequest", "webNavigation"]
...

In my case it listens onHistoryStateUpdated event to wake up the service worker:

// background.js
chrome.webNavigation.onHistoryStateUpdated.addListener((details) => {
  console.log('wake me up');
});

chrome.webRequest.onSendHeaders.addListener(
  (details) => {
    // code here
  },
  {
    urls: ['https://example.com/api/*'],
    types: ['xmlhttprequest'],
  },
  ['requestHeaders']
);
Agung Darmanto
  • 354
  • 2
  • 8
  • This did it for me, thanks a lot! Very simple and to the point (in my case I need to capture some data from a certain website when the user navigates it, so I just added the onHistoryStateUpdated and the rest remained the same) – pimguilherme May 21 '22 at 18:16
2

I found a different solution to keeping the extension alive. It improves on wOxxOm's answer by using a secondary extension to open the connection port to our main extension. Then both extensions try to communicate with each other in the event that any disconnects, hence keeping both alive.

The reason this was needed was that according to another team in my company, wOxxOm's answer turned out to be unreliable. Reportedly, their SW would eventually fail in an nondeterministic manner.

Then again, my solution works for my company as we are deploying enterprise security software, and we will be force installing the extensions. Having the user install 2 extensions may still be undesirable in other use-cases.

LetsDoThis
  • 39
  • 4
  • This does not provide an answer to the question. Once you have sufficient [reputation](https://stackoverflow.com/help/whats-reputation) you will be able to [comment on any post](https://stackoverflow.com/help/privileges/comment); instead, [provide answers that don't require clarification from the asker](https://meta.stackexchange.com/questions/214173/why-do-i-need-50-reputation-to-comment-what-can-i-do-instead). - [From Review](/review/late-answers/31288692) – Tom Mar 18 '22 at 09:31
-3

WebSocket callbacks registered from within the chrome.runtime listener registrations of my extensions's service worker would not get invoked, which sounds like almost the same problem.

I approached this problem by making sure that my service worker never ends, by adding the following code to it:

function keepServiceRunning() {
    setTimeout(keepServiceRunning, 2000);
  }

keepServiceRunning()

After this, my callbacks now get invoked as expected.

matanster
  • 14,583
  • 17
  • 83
  • 152
  • This doesn't work per the specification of service workers. You must have had devtools open for the service worker, which keeps the worker active intentionally circumventing the specification's timeouts to simplify debugging. – wOxxOm Aug 14 '21 at 05:30
  • Actually, I'm slightly confused, as my suggested answer code does keep the service alive indefinitely without devtools being open. Chromium Beta Version 93.0.4577.51 (Official Build) beta (64-bit). – matanster Aug 24 '21 at 23:16
  • It means there's a bug in the browser or something in your script is using the extended 5-minute timeout e.g. ports, messages, and a few other things. – wOxxOm Aug 24 '21 at 23:33
  • Thanks, I have added my use case to [crbug.com/1152255](https://bugs.chromium.org/p/chromium/issues/detail?id=1152255#c37) then, as I'm not explicitly using the extended timeout in any way I'm aware of – matanster Aug 24 '21 at 23:56
  • Without [MCVE](/help/mcve) I can't tell what's wrong. I only verified that it doesn't work in several different versions of Chrome including 93 per the specification. Note that chrome.runtime messaging is one of things that enable the extended 5-minute timeout. – wOxxOm Aug 25 '21 at 00:15