Skip to content

[firestore] web: one-shot Query.get() Future never settles when the server rejects the Listen target with permission-denied (code 7) — no resolve, no throw #18366

@theRoadz

Description

@theRoadz

Is there an existing issue for this?

  • I have searched the existing issues.

Which plugins are affected?

Database (cloud_firestore) — web only.

Which platforms are affected?

Web (reproduced on BOTH dart2js and dart2wasm builds; native Android/iOS behave correctly).

Description

On web, when a one-shot query get() is denied by Firestore security rules at the query-provability layer ("rules are not filters"), the server responds on the WebChannel Listen stream with a target rejection:

targetChange REMOVE targetIds:[98] cause:{code:7, message:"Missing or insufficient permissions."}

The JS SDK releases the target and applies the remote event — but the Dart Future returned by Query.get() neither resolves nor throws. No console error either. The await hangs forever.

The same query under the same rules on native SDKs throws FirebaseException(permission-denied), as expected.

This makes a class of rules misconfigurations effectively undebuggable on web: the app shows an eternal spinner with zero error surface anywhere (no Dart exception, no zone error, no console output). We only located it by enabling setLoggingEnabled(true) and reading the WebChannel frames.

Reproduction

  1. Security rules where a collection's allow read includes a resource.data condition that a query does not prove, e.g.:

    match /items/{id} {
      allow read: if resource.data.status == null;
    }
    
  2. From a signed-in web client, issue a query that does not include a status filter:

    final qs = await FirebaseFirestore.instance
        .collection('items')
        .where('logical_id', isEqualTo: 'x')
        .where('version_status', isEqualTo: 'current')
        .limit(1)
        .get(); // hangs forever on web; throws permission-denied on native
  3. Observed on web with persistence enabled AND disabled (both reproduce), dart2js AND dart2wasm, against production Firestore (not the emulator-only).

Observed SDK-debug trace (abridged)

With FirebaseFirestore.setLoggingEnabled(true) (JS setLogLevel('debug')), Chrome console:

[app] before-get-await (query: items, logical_id == 3rTNnYdOcCgM7MFor2mD AND version_status == current, limit 1)
WatchChangeAggregator: addTarget 90 (query shape above) sent on Listen stream
RemoteStore: targetChange REMOVE targetIds:[90] cause:{code:7, "Missing or insufficient permissions."}
SyncEngine: releases target 90, applies remote event
→ no error reaches the Dart Future; "after-get-await" never prints; no exception, no zone error

Control observation in the same session: per-DOCUMENT targets (addTarget.documents) under the same rules resolve normally with CURRENT + resume tokens — only the rules-unprovable QUERY shape is rejected, and only the rejection path loses the error.

Expected behavior

Query.get() on web completes with FirebaseException(code: 'permission-denied'), matching the native SDKs.

Versions

  • cloud_firestore: 5.6.12 (cloud_firestore_web: 4.4.12)
  • firebase-js-sdk (bundled): 11.9.1
  • firebase_core: 3.9.x
  • Flutter: 3.41.8 (stable)
  • Browser: Chrome on Windows 11 (also reproduced in fresh incognito sessions)

Additional context — related sharp edge

While investigating we also hit cloud_firestore_web's runTransaction, which wraps the JS transaction in a hidden Dart-side .timeout(30s) (cloud_firestore_web.dart:106-125). When the transaction wedges (e.g. its internal read hits the swallowed rejection above), the caller gets a bare TimeoutException — not a FirebaseException — so error handling written against the documented Firestore exception surface never sees it. If the silent-swallow above is fixed in the JS SDK, this mostly disappears; otherwise consider surfacing it as a FirebaseException (or documenting the TimeoutException).

If triage points at firebase-js-sdk rather than the FlutterFire bindings, happy to re-file there — the swallow is observable at the WebChannel/getDocs layer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    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