fix(storage emulator): don't report request failures as "document not found" in firestore.get() - #10883
Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
There was a problem hiding this comment.
Code Review
This pull request introduces a retry mechanism for firestore.get() and firestore.exists() calls in Storage rules to resolve intermittent connection failures during Firestore emulator startup. Feedback on the changes highlights a style guide violation regarding the use of any in the catch block. Additionally, the reviewer recommended separating the network request from response parsing to prevent standard JavaScript errors (such as TypeError) from incorrectly triggering retry attempts.
| for (let attempt = 1; attempt <= FETCH_FIRESTORE_DOCUMENT_MAX_ATTEMPTS; attempt++) { | ||
| try { | ||
| const doc = await client.get(pathname); | ||
| const { name, fields } = doc.body as { name: string; fields: string }; | ||
| const result = { name, fields }; | ||
| return { result, status: DataLoadStatus.OK, warnings: [], errors: [] }; | ||
| } catch (e: any) { | ||
| // A confirmed 404 from a server that's actually up means the document | ||
| // genuinely doesn't exist — that's a real answer, not a symptom of the | ||
| // emulator still starting up, so it's returned immediately either way. | ||
| const isConfirmedNotFound = e?.status === 404; | ||
| const isLastAttempt = attempt === FETCH_FIRESTORE_DOCUMENT_MAX_ATTEMPTS; | ||
| if (isConfirmedNotFound || isLastAttempt) { | ||
| return { status: DataLoadStatus.NOT_FOUND, warnings: [], errors: [] }; | ||
| } | ||
| await utils.sleep(FETCH_FIRESTORE_DOCUMENT_RETRY_DELAY_MS); | ||
| } | ||
| } |
There was a problem hiding this comment.
The current implementation uses e: any in the catch block, which violates the repository style guide rule: Never use any or unknown as an escape hatch. Define proper interfaces/types or use type guards.
Additionally, performing response destructuring inside the try block means standard JS errors (such as a TypeError if doc.body is null or undefined) will be caught and trigger retry attempts and sleeping. This is inefficient and masks programming/schema errors as transient network issues.
To address both issues, we can:
- Type
easunknownand use a safe type assertion to check the status code. - Separate the network request (
client.get) from the response parsing. - Add defensive checks to ensure
doc.bodyandbody.nameare valid before using them.
for (let attempt = 1; attempt <= FETCH_FIRESTORE_DOCUMENT_MAX_ATTEMPTS; attempt++) {
let doc;
try {
doc = await client.get(pathname);
} catch (e: unknown) {
const isConfirmedNotFound =
typeof e === "object" &&
e !== null &&
"status" in e &&
(e as { status?: number }).status === 404;
const isLastAttempt = attempt === FETCH_FIRESTORE_DOCUMENT_MAX_ATTEMPTS;
if (isConfirmedNotFound || isLastAttempt) {
return { status: DataLoadStatus.NOT_FOUND, warnings: [], errors: [] };
}
await utils.sleep(FETCH_FIRESTORE_DOCUMENT_RETRY_DELAY_MS);
continue;
}
const body = doc.body as { name?: string; fields?: unknown } | undefined;
if (!body || typeof body.name !== "string") {
return { status: DataLoadStatus.NOT_FOUND, warnings: [], errors: [] };
}
const result = { name: body.name, fields: body.fields };
return { result, status: DataLoadStatus.OK, warnings: [], errors: [] };
}References
- Never use any or unknown as an escape hatch. Define proper interfaces/types or use type guards. (link)
|
@googlebot I signed it! |
90f9f80 to
87b9109
Compare
|
@googlebot I signed it! |
… found" fetchFirestoreDocument() backs firestore.get()/firestore.exists() inside Storage rules. It caught every error from its request to the Firestore emulator and returned DataLoadStatus.NOT_FOUND, with no retry and nothing logged — so a confirmed 404 and a request that never reached a healthy server were reported identically. A rule gating on firestore.get() would then deny access on the strength of a transport failure. The try also wrapped the response destructuring, so a malformed payload (e.g. a null body) threw a TypeError into the same catch and was likewise reported as "not found", hiding a schema or programming error. - Only a confirmed HTTP 404 is treated as a real answer and returned immediately, leaving the common "does this exist yet" path unchanged. - Other failures are retried a small, bounded number of times, since a request that didn't complete says nothing about the document. - Response parsing moved outside the try, with a defensive check on body.name. - Errors narrowed with a hasHttpStatus type guard rather than `any`. Adds unit tests for the success path, the confirmed-404 fast path, a retried-then-successful connection failure, a malformed body, and a persistent non-404 failure.
87b9109 to
8a0990c
Compare
Description
fetchFirestoreDocument()insrc/emulator/storage/rules/runtime.tsthe pathbacking
firestore.get()/firestore.exists()inside Storage rules collapsesevery possible failure into "the document does not exist":
A confirmed HTTP 404 and a request that never reached a healthy server are
reported identically, with no retry and nothing logged. So a connection refusal,
a timeout, or a malformed response all cause a rule like:
to evaluate as though the document is absent, and deny the write indistinguishable,
from the caller's side, from a genuine permissions failure.
Because the
tryalso wraps the response destructuring, a malformed payload(e.g. a null
body) throws aTypeErrorthat lands in the same catch and islikewise reported as "not found", hiding a schema or programming error.
Fix
immediately so the common "does this exist yet" path is unchanged and stays fast.
on the grounds that it reflects the request not completing rather than an answer
about the document.
try, so a malformed payload surfaces asthe error it is instead of being retried as a network problem.
hasHttpStatustype guard rather thanany.Scope and honesty about evidence
I hit a run of failures where Storage rules using
firestore.get()behaved asthough the referenced document was missing under
emulators:exec, which is whatled me here. I have not been able to reproduce that failure on demand since
I tried a dropped page cache, cold JARs, and heavy CPU contention, and the
unpatched code passed every time. I also never captured the swallowed error
itself, so I can't tell you which error it was.
I've therefore deliberately avoided asserting a specific root cause in this PR.
What I'm proposing stands on the code as written: conflating "the request failed"
with "the document doesn't exist" produces incorrect rule evaluations, and the
current code cannot distinguish them. If maintainers consider the existing
behaviour intentional, I'm happy to close this.
Testing
Unit tests added in
src/emulator/storage/rules/runtime.spec.ts:NOT_FOUNDafter exactly one call (no retries)NOT_FOUNDnpm run test:compileandnpm run lint:quietpass; theruntime.spec.tssuite(11 tests) passes locally. I have not run the integration suites, which need
emulators and credentials I don't have configured.