firebase / firebase/firebase-js-sdk
Firestore client reports "client is offline" for one browser profile, persistently across reload and across client recreation, while plain HTTP to the same emulator endpoint succeeds in 5ms
- Dominant language
- TypeScript
- Stars
- 5.1k
- Forks
- 1k
- Avg merge
- 2d 21h
- Merged PRs (30d)
- 37
Description
### Operating System
Windows 11
### Environment (if applicable)
Chrome, Firebase JS SDK, Firestore + Auth emulators (local)
### Firebase SDK Version
Firebase JS SDK 12.16.0
### Firebase SDK Product(s)
Firestore
### Project Tooling
React, TypeScript, TanStack Start, Vite, Cloudflare Workers
### Detailed Problem Description
## Environment
- `firebase` JS SDK, Web (modular), browser: Chromium via Puppeteer, headless
- `initializeFirestore(app, { experimentalAutoDetectLongPolling: true })`, **no `localCache`** (so the
default memory cache — nothing is persisted to IndexedDB)
- `connectFirestoreEmulator(db, "127.0.0.1", 8099)`, Firebase CLI Firestore emulator
- Auth and Storage emulators also in use; project id `demo-…`
## Summary
After a period of ordinary authenticated use in one browser profile, the Firestore client stops being
able to read. `getDoc` never settles; `getDocFromServer` rejects after ~10s with:
```
FirebaseError: [code=unavailable]: Failed to get document because the client is offline
```
**In the same page, at the same moment, a plain `fetch()` to the same host, the same port and the same
document returns 200 with its contents in 5ms.** So the transport is demonstrably working and the SDK
will not use it.
## What makes this odd rather than a plain connectivity problem
| observation | detail |
|---|---|
| a plain `fetch` to the identical document/port | **200 with contents, 5ms** (dead-port control in the same run correctly reports no response) |
| a **full page reload** | does **not** recover it — new JS context, new client, new WebChannel |
| a **fresh browser profile**, same moment, same document | reads it normally in **14ms** |
| a **brand new `FirebaseApp`** in the wedged page, own `initializeFirestore`, `getDocFromServer` | fails identically |
| `experimentalForceLongPolling: true` (fresh instance) | fails identically |
| `experimentalAutoDetectLongPolling: false, experimentalForceLongPolling: false` | fails identically |
| all three of the above, on a **healthy** page | all answer, 8–19ms |
| Firestore IndexedDB stores | **none exist** (memory cache), so nothing of Firestore's is persisted |
| Listen `addTarget` / `removeTarget` on the wire | balance exactly; no leaked targets |
So it is not the document, not the backend, not the network path, not leaked listeners, not the client
instance, and not transport selection — and it is not persisted Firestore state, because there isn't
any. It behaves as a condition carried by the **browser profile**, above transport selection, that a
reload does not clear and a new client inherits.
## Reproduction
**Honest position first: I have not managed to reduce this to a standalone script, and I would rather
say so than hand you something that passes.** The reliable reproduction is application-level. What a
minimal page does *not* do is in the next section, and it may be the more useful half.
Application-level, reproduces ~10/10:
1. Sign in a user against the Auth emulator.
2. Perform ordinary authenticated activity — a few page loads that each read a couple of documents.
Creating a record then navigating reproduces it about 10/10 here; loading two signed-in routes
reproduces it often.
3. Load a page that reads a single document with `getDoc`.
4. Observe: the read never settles; a `fetch()` to
`http://127.0.0.1:8099/v1/projects//databases/(default)/documents//` from the
same page returns 200 immediately.
## What a minimal standalone page does NOT reproduce
A standalone harness was built for this — its own `firebase.json`, wide-open rules, a page bundled from
`node_modules`, the Firestore and Auth emulators, puppeteer, no application code. **None of these
reproduce it**, every run ending with the SDK and a plain `fetch` both succeeding in 10–25ms:
| attempt | result |
|---|---|
| 6 page loads, 4 anonymous `getDoc`s each | no |
| the same, authenticated (anonymous sign-in) | no |
| 14 loads, 25 authenticated `getDoc`s each — 350 reads | no |
| 14 loads, 6 persistent `onSnapshot` listeners each, never unsubscribed — 84 streams | no |
Authentication was tried because it is evidence-backed rather than guessed: in the application, **8
page loads reading public data stay clean while 2 signed-in loads wedge**. Listeners were tried as the
most likely remaining axis, on the reasoning that a long-lived stream drives the connection machinery
in a way a one-shot `get` does not.
**So the condition is not plain document reads, not authentication alone, not volume, and not
listeners** — and the application reaches it with far fewer operations than 350. That points at the
*kind* of activity rather than the amount. Untried: real queries, interleaved writes, several
collections, and Storage, which the application also uses on the reproducing path.
The harness is available and takes seconds to extend if any of those is a likely candidate to you.
## Why this is being reported despite being emulator-only
It has only been observed against the Firestore emulator, and that is stated plainly rather than
glossed. Two results that would have pointed at an emulator artefact were specifically looked for and
**did not appear**: a wedged socket pool (ruled out — plain HTTP works), and a transport-detection
failure specific to the emulator's long polling (ruled out — forcing either transport fails
identically). What remains is a client-side condition in the same SDK that runs against production.
## Impact
An app cannot detect or recover from this state from inside the page: recreating the client does not
help, and the SDK reports `unavailable`, which is indistinguishable from ordinary offline. Our
mitigation is a bounded timeout on account reads plus a screen that says the account could not be
loaded rather than rendering a signed-out state at a signed-in user — which is a fair error message,
not a recovery.
### Steps and code to reproduce issue
## Environment
- `firebase` JS SDK, Web (modular), browser: Chromium via Puppeteer, headless
- `initializeFirestore(app, { experimentalAutoDetectLongPolling: true })`, **no `localCache`** (so the
default memory cache — nothing is persisted to IndexedDB)
- `connectFirestoreEmulator(db, "127.0.0.1", 8099)`, Firebase CLI Firestore emulator
- Auth and Storage emulators also in use; project id `demo-…`
## Summary
After a period of ordinary authenticated use in one browser profile, the Firestore client stops being
able to read. `getDoc` never settles; `getDocFromServer` rejects after ~10s with:
```
FirebaseError: [code=unavailable]: Failed to get document because the client is offline
```
**In the same page, at the same moment, a plain `fetch()` to the same host, the same port and the same
document returns 200 with its contents in 5ms.** So the transport is demonstrably working and the SDK
will not use it.
## What makes this odd rather than a plain connectivity problem
| observation | detail |
|---|---|
| a plain `fetch` to the identical document/port | **200 with contents, 5ms** (dead-port control in the same run correctly reports no response) |
| a **full page reload** | does **not** recover it — new JS context, new client, new WebChannel |
| a **fresh browser profile**, same moment, same document | reads it normally in **14ms** |
| a **brand new `FirebaseApp`** in the wedged page, own `initializeFirestore`, `getDocFromServer` | fails identically |
| `experimentalForceLongPolling: true` (fresh instance) | fails identically |
| `experimentalAutoDetectLongPolling: false, experimentalForceLongPolling: false` | fails identically |
| all three of the above, on a **healthy** page | all answer, 8–19ms |
| Firestore IndexedDB stores | **none exist** (memory cache), so nothing of Firestore's is persisted |
| Listen `addTarget` / `removeTarget` on the wire | balance exactly; no leaked targets |
So it is not the document, not the backend, not the network path, not leaked listeners, not the client
instance, and not transport selection — and it is not persisted Firestore state, because there isn't
any. It behaves as a condition carried by the **browser profile**, above transport selection, that a
reload does not clear and a new client inherits.
## Reproduction
**Honest position first: I have not managed to reduce this to a standalone script, and I would rather
say so than hand you something that passes.** The reliable reproduction is application-level. What a
minimal page does *not* do is in the next section, and it may be the more useful half.
Application-level, reproduces ~10/10:
1. Sign in a user against the Auth emulator.
2. Perform ordinary authenticated activity — a few page loads that each read a couple of documents.
Creating a record then navigating reproduces it about 10/10 here; loading two signed-in routes
reproduces it often.
3. Load a page that reads a single document with `getDoc`.
4. Observe: the read never settles; a `fetch()` to
`http://127.0.0.1:8099/v1/projects//databases/(default)/documents//` from the
same page returns 200 immediately.
## What a minimal standalone page does NOT reproduce
A standalone harness was built for this — its own `firebase.json`, wide-open rules, a page bundled from
`node_modules`, the Firestore and Auth emulators, puppeteer, no application code. **None of these
reproduce it**, every run ending with the SDK and a plain `fetch` both succeeding in 10–25ms:
| attempt | result |
|---|---|
| 6 page loads, 4 anonymous `getDoc`s each | no |
| the same, authenticated (anonymous sign-in) | no |
| 14 loads, 25 authenticated `getDoc`s each — 350 reads | no |
| 14 loads, 6 persistent `onSnapshot` listeners each, never unsubscribed — 84 streams | no |
Authentication was tried because it is evidence-backed rather than guessed: in the application, **8
page loads reading public data stay clean while 2 signed-in loads wedge**. Listeners were tried as the
most likely remaining axis, on the reasoning that a long-lived stream drives the connection machinery
in a way a one-shot `get` does not.
**So the condition is not plain document reads, not authentication alone, not volume, and not
listeners** — and the application reaches it with far fewer operations than 350. That points at the
*kind* of activity rather than the amount. Untried: real queries, interleaved writes, several
collections, and Storage, which the application also uses on the reproducing path.
The harness is available and takes seconds to extend if any of those is a likely candidate to you.
## Why this is being reported despite being emulator-only
It has only been observed against the Firestore emulator, and that is stated plainly rather than
glossed. Two results that would have pointed at an emulator artefact were specifically looked for and
**did not appear**: a wedged socket pool (ruled out — plain HTTP works), and a transport-detection
failure specific to the emulator's long polling (ruled out — forcing either transport fails
identically). What remains is a client-side condition in the same SDK that runs against production.
## Impact
An app cannot detect or recover from this state from inside the page: recreating the client does not
help, and the SDK reports `unavailable`, which is indistinguishable from ordinary offline. Our
mitigation is a bounded timeout on account reads plus a screen that says the account could not be
loaded rather than rendering a signed-out state at a signed-in user — which is a fair error message,
not a recovery.
Contributor guide
Assessment
This issue has not been assessed yet.