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.

Open
#10,197 0 comments 0 reactions 0 assignees View on GitHub
api: messaging needs-attention question Repro Needed
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.