Is there an existing issue for this?
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
-
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;
}
-
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
-
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.
Is there an existing issue for this?
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:The JS SDK releases the target and applies the remote event — but the Dart
Futurereturned byQuery.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
Security rules where a collection's
allow readincludes aresource.datacondition that a query does not prove, e.g.:From a signed-in web client, issue a query that does not include a
statusfilter: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)(JSsetLogLevel('debug')), Chrome console:Control observation in the same session: per-DOCUMENT targets (
addTarget.documents) under the same rules resolve normally withCURRENT+ 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 withFirebaseException(code: 'permission-denied'), matching the native SDKs.Versions
cloud_firestore: 5.6.12 (cloud_firestore_web: 4.4.12)firebase_core: 3.9.xAdditional context — related sharp edge
While investigating we also hit
cloud_firestore_web'srunTransaction, 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 bareTimeoutException— not aFirebaseException— 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 aFirebaseException(or documenting theTimeoutException).If triage points at firebase-js-sdk rather than the FlutterFire bindings, happy to re-file there — the swallow is observable at the WebChannel/
getDocslayer.