Skip to content

@firebase/firestore: ca9/b815 assertion after first cold getDoc() with memoryLocalCache #10008

@ShaneBreazeale

Description

@ShaneBreazeale

Operating System

Not OS-specific so far. Observed in a browser SPA environment.

Environment (if applicable)

  • React 19
  • Vite SPA
  • Firestore Web SDK initialized with memoryLocalCache()
  • experimentalAutoDetectLongPolling enabled

Firebase SDK Version

firebase@12.13.0

Also reproduced across 12.2.0 through 12.13.0, so this does not appear to be specific to 12.3.0.

Firebase SDK Product(s)

Firestore

Project Tooling

  • React 19
  • Vite
  • Browser client app

Detailed Problem Description

This is a follow-up to #9267 (comment) after #9267 was closed as fixed by #9842. We are still seeing the same ca9 / b815 failure on firebase@12.13.0.

In our app, the deterministic trigger is that the first Firestore operation after page load is a single server getDoc() against a freshly-opened WebChannel:

const db = initializeFirestore(app, {
  localCache: memoryLocalCache(),
  experimentalAutoDetectLongPolling: true,
});

// First op on the cold stream:
await getDoc(doc(db, 'users', uid));

The ca9 assertion fires roughly 60ms after that getDoc() resolves, which points to the transient listen teardown path rather than the add path. The stack is consistently in the watch path:

__PRIVATE_TargetState.recordTargetResponse
  hardAssert(this.outstandingResponses >= 0, 0xca9)
__PRIVATE_WatchChangeAggregator.handleTargetChange
... forEachTarget -> onWatchStreamChange

The observed internal state is TargetState.outstandingResponses === -1, which suggests a watch Removed / Added target-change is being processed without a matching recordPendingTargetRequest().

The more severe behavior is the cascade after the initial assertion:

  1. 0xca9 is thrown from the watch target bookkeeping path.
  2. The throw propagates into AsyncQueue.
  3. AsyncQueue permanently fails.
  4. Every subsequent Firestore operation for the page lifetime throws b815 via verifyNotFailed.

So a single unexpected target response effectively bricks the entire Firestore client until the page is reloaded.

Expected Behavior

A transient watch target bookkeeping mismatch should not permanently fail the Firestore client. At worst, the affected target should be dropped, recovered, or re-listened.

Actual Behavior

The SDK hard-asserts with ca9, then the entire AsyncQueue remains failed and later reads/listeners/writes fail with b815.

Workaround

Replacing the first transient getDoc() with a persistent onSnapshot() eliminated the issue across many cold loads in our app. The listener resolves the first server snapshot but remains open, avoiding the immediate remove-target teardown race.

Practical workaround:

// Avoid making a transient getDoc() the first operation on a cold WebChannel.
// Use a kept-open listener for the first read instead.
const unsubscribe = onSnapshot(doc(db, 'users', uid), snapshot => {
  // Resolve first server snapshot, but keep listener open.
});

Suggested SDK Fix

Treat an unexpected extra target response as a recoverable inconsistency instead of a hard assertion that kills the shared AsyncQueue. Recovering the affected target or re-listening would turn this into a localized read/listen failure instead of a total page-lifetime Firestore outage.

Steps and code to reproduce issue

The minimal trigger in our app is:

  1. Initialize Firestore with memoryLocalCache() and experimentalAutoDetectLongPolling.
  2. On a fresh page load / cold Firestore client, make the first Firestore operation a single getDoc().
  3. Allow the transient listen to resolve and tear down.
  4. Observe ca9 shortly after the getDoc() resolves.
  5. Observe subsequent operations fail with b815 for the rest of the page lifetime.
import { initializeApp } from 'firebase/app';
import {
  doc,
  getDoc,
  initializeFirestore,
  memoryLocalCache,
} from 'firebase/firestore';

const app = initializeApp(firebaseConfig);
const db = initializeFirestore(app, {
  localCache: memoryLocalCache(),
  experimentalAutoDetectLongPolling: true,
});

await getDoc(doc(db, 'users', uid));

// ca9 fires shortly after this resolves; subsequent Firestore operations fail with b815.

I can't reproduce the issue in a test repo, will try again this week.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions