Closed jkinger closed 1 month ago
Hi, @jkinger. Thanks for reporting this.
Looks like Chrome has changed the way it deactivates idle service workers.
We have a ping/pong messaging in place to make sure the client constantly messages the worker so it never goes idle:
Previously, the worker was considered idle if it hasn't handled any messages in the N period of time. Now, it looks like the criteria for that changed.
I can see that Google has changed the Service Worker termination rules for Service Workers in extensions:
The service worker has not received an event for over thirty seconds and there are no outstanding long running tasks in progress. If a service worker received an event during that time, the idle timer was removed.
This should have no effect on MSW as it's not an extension but a Service Worker registered by your app.
I've found another mention of this behavior that doesn't seem to be related to extension Service Workers but generally to Service Workers in Chrome:
The browser can terminate a running SW thread at almost any time. Chrome terminates a SW if the SW has been idle for 30 seconds. Chrome also detects long-running workers and terminates them. It does this if an event takes more than 5 minutes to settle, or if the worker is busy running synchronous JavaScript and does not respond to a ping within 30 seconds. When a SW is not running, Developer Tools and chrome://serviceworker-internals show its status as STOPPED.
If this is true, then the idle period has shortened from 5 minutes to 30 seconds.
It's worth mentioning this is a behavior specific only to the Manifest V3 API. Perhaps that's why it's not surfacing for everyone?
I wonder why Chrome doesn't do this to MSW's Service Worker:
Service workers don't live indefinitely. While exact timings differ between browsers, service workers will be terminated if they've been idle for a few seconds, or if they've been busy for too long. If a service worker has been terminated and an event occurs that would start it up, it will restart.
https://web.dev/learn/pwa/service-workers#service_worker_lifespan
From what I understand, the worker goes idle, then the browser terminates it, and then your requests fail. Is that correct, @jkinger?
A good thread on why Service Workers are killed after becoming idle: https://github.com/w3c/ServiceWorker/issues/980
I wonder if instead of trying to keep the worker alive and playing a guess game with the browser and its internal policies, we could detect that the browser has unregistered the worker (I do believe the worker's state change event should be emitted in that case) and if that happens, try to register and activate the worker again?
@kettanaito
Yes, all that you explained above sounds accurate as this just started happening within 6 months or so and only happens on Chromium-based browsers. Firefox doesn't have this issue.
Your last post about "..try to register and activate the worker again" is what I've been thinking about too. Could I re-instantiate the MSW service again somehow (like below) when the service worker is de-registered or starts throwing errors?
const { worker } = await import('./mocks/browser')
return worker.start()
Or would there be more to it? Or maybe something like a worker.restart()
?
I think it's not something you should be doing manually. I wonder if that unregistration that Chrome does trigger the state change event on the worker.
@jkinger, can you please try this out in your scenario?
navigator.serviceWorker.controller.addEventListener('statechange', (event) => {
console.log(event.target.state)
})
What does this print when Chrome terminates the idle worker? Does it print anything at all?
I hope Chrome doesn't go around the worker's lifecycle and the termination that it initiates still causes the worker to go into something like "redundant". If we can detect that via a listener, we can call something akin to worker.start()
when that state transition happens.
@kettanaito
I've taken your recommendation and code to see if I can get notified when Chrome stops the service worker but unfortunately not getting any events for "'statechange'" when Chrome stops it. I know what you're getting at so trying to explore other listeners as well.
FWIW another observation I've noticed is Chrome doesn't seem to unregister the Service Worker but only stops it. When I return to the Browser / Tab and start interacting with the page again Chrome starts the Server Worker again and it's still under the same Registration ID it was first registered with.
Thanks for your help thus far.
FWIW another observation I've noticed is Chrome doesn't seem to unregister the Service Worker but only stops it.
Yeah, this makes sense. Aligns with what Chrome is saying they do.
When I return to the Browser / Tab and start interacting with the page again Chrome starts the Server Worker again and it's still under the same Registration ID it was first registered with.
Does this "solve" the issue then? Can you describe to me the scenario when Chrome kills the worker and that causes issues?
Does this "solve" the issue then?
Yeah, you would think, right? But it doesn't, I've been monitoring the Service Worker via chrome://serviceworker-internals/?devtools and I see when Chrome stops it after the set time of inactivity and then I see it being started again when I start interacting with the page but nothing is being intercepted as before.
There is a disconnect happening somewhere during the Stop / Restart of this Worker that's keeping MSW inoperable and that is what I'm trying to pin down. If Chrome re-starts it again then why doesn't MSW start responding to requests again? That's the part that doesn't make sense. It's like the Starting, Stopping then Starting again throws it off somehow.
The only way I'm able to get MSW to start responding again is by doing a Refresh.
There is a disconnect happening somewhere during the Stop / Restart of this Worker that's keeping MSW inoperable and that is what I'm trying to pin down.
I can imagine if terminating the worker also removes any listeners attached to it, MSW will lose any connection with the worker. We rely on a MessageChannel to talk to the worker. If that gets shutdown and doesn't revive, MSW won't work.
The only way I'm able to get MSW to start responding again is by doing a Refresh.
This is a strong indication that's precisely what's happening.
I finally identified the root cause: when Chrome stops and then restarts the service worker, it creates a new instance of mockServiceWorker.js
. This action removes any previously registered Client IDs under activeClientIds
. To complicate matters, when Chrome restarts the service worker, it provides no indication (that I'm aware of) that a new instance was created. Consequently, no MOCK_ACTIVATE
message is posted to re-register a new active client ID.
Fortunately, all the listeners remain connected and active, so fetch
will still fire. However, since there are no activeClientIds
, the activeClientIds.size === 0
check will prevent it from proceeding.
I'm not yet sufficiently familiar with the MSW code to suggest a comprehensive solution, but I managed to devise a quick, albeit makeshift, fix. If implemented in the App before a fetch, it should temporarily resolve the issue. Here's what the fix looks like:
if ('serviceWorker' in navigator && navigator.serviceWorker.controller) {
navigator.serviceWorker.controller.postMessage('MOCK_ACTIVATE');
// Small wait for changes to propagate
await new Promise((resolve) => {
setTimeout(resolve, 10);
});
}
await fetch('endpoint', {...});
By calling MOCK_ACTIVATE
manually it registers a new client ID in activeClientIds
and allows the rest of the Service Worker's "fetch" to continue.
I've updated my Github repo (as well as the live site) with this fix, so anyone interested can see it in action.
It’s not the most elegant solution, but it should suffice until a more definitive fix is developed.
@kettanaito , any updates on this? Can we prioritize this issue? The worker shutdowns every 30 seconds.
@jkinger, thanks for sharing the findings! So Chrome does re-registers the worker but we are loosing the values stores on its global scope. Interesting. Due to how isolated workers are by design, I doubt there's plenty of ways for us to persist those values (active client ids).
I wonder if there's a way to catch that re-registration on the navigator.serviceWorker
level? Don't they fire some statechange
event or something?
worker.addEventListener('statechange', () => {
console.log('is this called?')
})
If we found a way for us to react to this behavior, we could re-establish the activation with the worker, which would add the current client into the active clients on the worker's side.
@pahieiev, I'm not looking into this issue right now. You (your company) can prioritize this issue by becoming our Golden sponsor. I would be happy to look into this in more detail then. You are also fee to take the initiative and debug this deeper.
My hope is that Chrome dispatches the controllerchange
event when it re-registers the idle worker. That sounds like a fitting event to dispatch—the client has acquired a new worker.
Unfortunately, no matter the idle timeout (5m, 10m), this is not reproducible in Playwright. I suspect this behavior is not a part of Chromium but Chrome. Testing it manually in Chrome then.
@jkinger, do you have a reproduction repo that can be publicly installed? The repo you linked loads its dependencies from private Artifactory. It won't install.
I've tried the live version to no success. The issue cannot be reproduce after 10m of idle tab, no devtools, on the latest Chrome.
I'm closing this until there's a reproduction for this.
Prerequisites
Environment check
msw
versionBrowsers
Chromium (Chrome, Brave, etc.)
Reproduction repository
https://github.com/jkinger/msw-react
Reproduction steps
You can either clone the provided repo or use the Github Pages link here: https://jkinger.github.io/msw-react/
If Cloning the Repo
App Url
Note: I had a similar issue open in v1 and closed in hopes this would be addressed in v2.
Current behavior
If the Chrome tab is left open (no DevTools) with no user activity for at least 5 minutes and you come back after that time you'll notice MSW stops intercepting requests.
Expected behavior
If the Chrome tab is left open with no user activity for at least 5 minutes and you come back after that time MSW should still be intercepting requests as before.