firebase / firebase/firebase-js-sdk
Messaging: getToken() fails with the error "Service worker not registered after 10000 ms", and waitForRegistrationActive does not account for cases where the observed worker becomes redundant.
- Dominant language
- TypeScript
- Stars
- 5.1k
- Forks
- 1k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 37
Description
### Operating System
macOS (observed), but the issue is OS-independent
### Environment (if applicable)
Chrome (latest stable)
### Firebase SDK Version
12.12.1 (`@firebase/messaging` 0.12.25)
### Firebase SDK Product(s)
Messaging
### Project Tooling
React app built with Vite
### Detailed Problem Description
`getToken()` intermittently fails with:
> Messaging: We are unable to register the default service worker. Service worker not registered after 10000 ms (messaging/failed-service-worker-registration).
even though the service worker registers and activates successfully (visible in DevTools → Application → Service Workers).
The cause appears to be in `waitForRegistrationActive`, introduced in #8661:
```js
async function registerDefaultSw(messaging) {
try {
messaging.swRegistration = await navigator.serviceWorker.register(DEFAULT_SW_PATH, {
scope: DEFAULT_SW_SCOPE
});
// ...
messaging.swRegistration.update().catch(() => { /* non blocking */ });
await waitForRegistrationActive(messaging.swRegistration);
}
// ...
}
function waitForRegistrationActive(registration) {
return new Promise((resolve, reject) => {
const rejectTimeout = setTimeout(
() => reject(new Error(`Service worker not registered after ${DEFAULT_REGISTRATION_TIMEOUT} ms`)),
DEFAULT_REGISTRATION_TIMEOUT
);
const incomingSw = registration.installing || registration.waiting;
if (registration.active) {
clearTimeout(rejectTimeout);
resolve();
} else if (incomingSw) {
incomingSw.onstatechange = ev => {
if (ev.target?.state === 'activated') {
incomingSw.onstatechange = null;
clearTimeout(rejectTimeout);
resolve();
}
};
} else {
clearTimeout(rejectTimeout);
reject(new Error('No incoming service worker found.'));
}
});
}
```
Two problems:
1. `waitForRegistrationActive` captures a single worker (`registration.installing || registration.waiting`) and listens for its `statechange` only, resolving only on `'activated'`. If that specific worker is replaced and transitions to `'redundant'`, `'activated'` never fires on it, and the promise hangs until the 10s timeout — even when a replacement worker activates successfully in the meantime.
2. The race partner is created by the SDK itself: `registerDefaultSw` calls `swRegistration.update()` immediately before awaiting `waitForRegistrationActive`. If that update check installs a new worker (e.g. the script or one of its `importScripts` dependencies changed, which is likely right after a redeploy), the captured worker can become redundant while being watched.
The `'redundant'` state is never handled, so the failure also cannot be reported early — the user always waits the full 10 seconds before getting an error.
I hit this in production-like conditions right after redeploying the site (i.e. exactly when the SW script content changes and the `update()` race is most likely). I could not build a deterministic minimal reproduction because the race is timing-dependent, but the mechanism above is directly visible in the shipped source.
I'm not sure what the best fix would be, but I think something along these lines would probably resolve it:
- re-checking `registration.active` before rejecting on timeout (or polling it), rather than relying solely on the `statechange` of the initially captured worker
- when the watched worker becomes `'redundant'`, re-acquiring `registration.installing || registration.waiting` and continuing to wait on the new worker
- following replacement workers via `registration.onupdatefound`
As a workaround, I'm currently registering the service worker in application code, waiting for `registration.active` myself, and passing the registration to `getToken()` via the `serviceWorkerRegistration` option. This bypasses `registerDefaultSw` entirely and has avoided the issue so far.
### Steps and code to reproduce issue
Timing-dependent; most likely to occur when:
1. A service worker for `/firebase-cloud-messaging-push-scope` was previously registered (or registration was recently attempted) and the SW script or its imported scripts have changed on the server.
2. Call `getToken(messaging, { vapidKey })` without passing `serviceWorkerRegistration`.
3. `registerDefaultSw` registers the SW, fires `update()`, and `waitForRegistrationActive` starts watching the pre-update worker.
4. The watched worker becomes `redundant`; the replacement worker activates; `getToken` still rejects after 10000 ms.
Contributor guide
Research direction
Start by reading registerDefaultSw and waitForRegistrationActive in the Messaging service-worker registration path, focusing on the immediate update() call and the watched worker's statechange handler. Reproduce or inspect the installing, waiting, active, and redundant transitions around getToken(). Done means replacement workers are handled or the active registration is detected without the spurious 10-second failure.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- firebase, typescript
- Domain
- api, web-dev
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100