Hello,
I am developing a Safari extension that uses service workers and all works well. When the extension is updated, a new content script is injected into the current open tabs however the service worker connection with the new content script does not get established.
I confirmed both the old content script and new one is on the page, but the new one just doesn't execute which means it will not connect to the service worker.
In Chrome a newly injected content script does work and in FireFox it is handled by FireFox automatically. How can this be done in Safari?
Any help would be appreciated.
Thanks for the detailed description. To look into this, I built a minimal Manifest V3 Safari web extension that mirrors your setup. It has a background service worker and a content script that opens a runtime.connect port to it and reports success or failure on the page. I then exercised it on shipping Safari 26.5, bumping the extension version repeatedly to trigger updates while a normal page stayed open.
Here is what I observed on that setup.
When the extension updates, a content script injected into an already-open tab is bound to the previous extension generation, not the new one. In my logs the injected script consistently reported the previous version string. Calls to connect() and postMessage on the service worker received no reply and no disconnect event. It never reached a live service worker, and it stayed that way. Reloading the page was the only thing that recovered it. After a reload, the freshly loaded content script ran under the new generation and connected on the first attempt.
In my case the injected script did run. It executed and attempted to connect; the failure was specifically in reaching the service worker. If your new script appears to "not execute," it may be worth confirming whether it is actually failing at the connection step instead. A silent connection failure can look like the script never running.
A few specifics that may match what you are seeing:
- The stranded content script does not look "invalidated."
chrome.runtime.idstayed defined the whole time, so the common "is my context still valid?" check does not catch this case. - Re-injecting again did not help. I re-ran the injection both from the update handler and from a toolbar action. Each new content script was still bound to the old generation and still could not reach the service worker.
- Retrying the connection on a timer did not help either, for the same reason. The service worker it is bound to is gone, and retrying does not rebind the script to the new generation.
On Safari, injecting a new content script into open tabs on update, then reconnecting, did not recover the connection for the transitioning tab. In my testing, that injected script belonged to the outgoing generation.
What did recover the tab was reloading it. When I changed the update handler to call chrome.tabs.reload on the affected tabs instead of re-injecting, the page reloaded. A fresh content script loaded under the new generation and connected normally with no further action.
The handling that worked here, then, is to reload the affected tabs after an update rather than re-inject into them. That carries a tradeoff: a reload discards in-page state. Depending on your extension's UX, you might instead prompt the person to reload the affected tabs. Or you could design the content script so a reload is harmless. Those are all reasonable; which one fits is your call.
On the cross-browser point: I only tested Safari here. I can't speak to what Chrome or Firefox do internally with re-injection and reconnection during an update. It is plausible their timing hides this window, but I can't verify that from this side.
You may believe Safari's behavior here, re-injecting a content script during an update, should work differently. If so, filing a report with Feedback Assistant would put it in front of the team. A minimal example would help: a background service worker plus a content script that connects on load, updated with the page left open. I am happy to share the minimal test extension I built if it would help you file or compare against your own.