Repository navigation
Firestore Rules emulator: denial message concatenates every match block's evaluation, and "Variable is not bound in path template." omits the variable name #11239
Description
Environment
firebase-tools15.22.4 (bundled Firestore emulator), macOS arm64- Driven by
@firebase/rules-unit-testing(initializeTestEnvironment) from the JS SDK
What happens
When a LIST (or GET) is denied, the emulator returns one FirebaseError whose message concatenates the evaluation output of every matching match block into a single comma-joined string. Two problems:
- No structure. Errors from different
matchblocks (a specific rule and a wildcard rule) are glued into one string, so it is impossible to tell which evaluation produced the final deny or what each fragment refers to. Withor()queries the same block's error repeats once per disjunctive branch, so a real ruleset easily produces 6+ fragments interleaving unrelated line numbers. Variable is not bound in path template.omits the variable name — note the double space; the name is empty. The message points at a line but not at which variable or which path template. In our case the variable is the recursive-wildcard binding (match /tenants/{tenantId}/{document=**}used asdocument[0]), a standard documented pattern, so the error reads as if the rule were malformed.
The combined effect sends debugging to the wrong line: you chase the wildcard line (or your own rule) while the operative deny may be a completely different evaluation (e.g. a missing document field), and vice versa. We lost a full triage session to this: two failing tests were stale seeds missing a field, but the message interleaved them with the wildcard's Variable is not bound, so the first hypothesis was a broken security rule / emulator bug.
Minimal repro
Rules (11 lines):
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /tenants/{tenantId}/docs/{docId} {
allow list: if resource.data.accessLevel == 'public';
}
match /tenants/{tenantId}/{document=**} {
allow read: if document[0] != 'docs';
}
}
}
Seed with security rules disabled: document tenants/T1/docs/a = { departmentIds: [] } (note: no accessLevel field).
Then, as an authenticated member of T1:
await getDocs(query(
collection(db, 'tenants/T1/docs'),
or(
where('departmentIds', '==', []),
where('departmentIds', 'array-contains-any', ['d1']),
),
));Actual message (verbatim, one FirebaseError):
Property accessLevel is undefined on object. for 'list' @ L5, Variable is not bound in path template. for 'list' @ L8
@ L5is the specific match's real cause: the seeded document lacksaccessLevel.@ L8is the wildcard block erroring while evaluatingdocument[0]during the list demonstration — with the variable name missing from the message.
Against our production ruleset (~2.5k lines) the same shape produces six comma-joined fragments interleaving two unrelated line numbers, e.g.:
evaluation error at L1294:24 for 'list' @ L1294, Variable is not bound in path template. for 'list' @ L2491, evaluation error at L1294:24 for 'list' @ L1294, Variable is not bound in path template. for 'list' @ L2491, Property accessLevel is undefined on object. for 'list' @ L1294, Variable is not bound in path template. for 'list' @ L2491
Expected
One structured error per failing evaluation (or a structured list), each including:
- the
matchblock path that was evaluated, - line/column,
- for
Variable is not bound in path template.: the variable name and the path template it refers to,
and ideally which evaluation(s) actually caused the final deny.
Notes
- The deny decision in the repro is correct (fail-closed: the document lacks the field). This report is only about the diagnostic value of the message.
- Possibly related runtime quirk worth a look:
document[0]inside a{param=**}match evaluates fine for single-document GETs in our suite (hundreds of passing rules tests against this ruleset) but yieldsVariable is not bound in path template.during list demonstration.
Activity