fix(emulator): warn instead of crashing when the Firestore rules watcher fails - #11042
fix(emulator): warn instead of crashing when the Firestore rules watcher fails#11042gauranshahuja wants to merge 7 commits into
Conversation
…er fails The chokidar watcher for firestore.rules had no error listener, so a watcher failure (for example no inotify support in some CI or Docker environments) was thrown from the EventEmitter and terminated the CLI. Log a labeled warning, keep the emulator running, and point at CHOKIDAR_USEPOLLING as the workaround. Addresses firebase#4298
There was a problem hiding this comment.
Code Review
This pull request ensures that the Firestore emulator logs a warning instead of crashing the CLI when the rules file watcher fails, and adds comprehensive unit tests to verify this behavior. The feedback suggests defensively handling the error parameter in the watcher's error listener as unknown and using a type guard to safely extract the message, preventing potential runtime TypeErrors.
| this.rulesWatcher.on("error", (err: Error) => { | ||
| utils.logLabeledWarning( | ||
| "firestore", | ||
| `Could not watch ${rulesPath} for changes, so rules hot reloading is disabled: ${err.message}. ` + | ||
| "If your environment does not support file watching, set CHOKIDAR_USEPOLLING=1 to fall back to polling.", | ||
| ); | ||
| }); |
There was a problem hiding this comment.
If err is null, undefined, or not an instance of Error at runtime, accessing err.message will throw a TypeError (e.g., Cannot read properties of undefined (reading 'message')), which would crash the emulator process and defeat the purpose of this fix.
To prevent this, we should defensively handle the error parameter by typing it as unknown and using a type guard (like instanceof Error) to safely extract the message, falling back to String(err) otherwise. This also aligns with the repository style guide's recommendation to use type guards instead of using any or unknown as an escape hatch.
this.rulesWatcher.on("error", (err: unknown) => {
const errMsg = err instanceof Error ? err.message : String(err);
utils.logLabeledWarning(
"firestore",
"Could not watch " + rulesPath + " for changes, so rules hot reloading is disabled: " + errMsg + ". " +
"If your environment does not support file watching, set CHOKIDAR_USEPOLLING=1 to fall back to polling.",
);
});References
- Never use
anyorunknownas an escape hatch. Define proper interfaces/types or use type guards. (link)
…ror handler Addresses the review suggestion on firebase#11042: type the listener argument as unknown and fall back to String(err) when it is not an Error.
|
|
||
| afterEach(() => { | ||
| sandbox.restore(); | ||
| fs.rmSync(tmpDir, { recursive: true, force: true }); |
There was a problem hiding this comment.
nit: before rmSync let's do rulesWatcher.close()
There was a problem hiding this comment.
Done in a167500: afterEach now awaits rulesWatcher.close() before rmSync, so a test that fails before reaching stop() no longer leaves a watcher open on the temp dir. The four specs, eslint and tsc are clean locally.
…fail-gracefully # Conflicts: # CHANGELOG.md
…fail-gracefully # Conflicts: # CHANGELOG.md
Description
Addresses #4298.
FirestoreEmulator.start()creates a chokidar watcher for the configuredfirestore.rulesfile so that rules can be hot reloaded. The watcher never had an"error"listener. Node'sEventEmitterthrows when"error"is emitted with no listener, so any watcher failure (the issue reportsError: Unable to watch changes in file "/path/firestore.rules"on CI images without inotify support) took down the wholefirebase emulators:startprocess instead of just disabling hot reload.This PR adds an
"error"handler that logs a labeled warning with the rules path, the underlying message, and theCHOKIDAR_USEPOLLING=1workaround suggested in the issue, then lets the emulator keep running.What is intentionally not in this PR:
if (!process.env.CI)skip suggested by @yuchenshi in the issue thread. That changes behaviour for every CI user who relies on hot reload, so I left that decision to maintainers; this change is strictly "do not crash". Happy to add it in this PR if preferred.DatabaseEmulatorhas the same watcher without an error listener (src/emulator/databaseEmulator.ts). I kept this PR to the Firestore emulator reported in Disable firestore.rules file monitor on CI #4298 and can follow up for the Database emulator if wanted.Scenarios Tested
Unit tests (new
src/emulator/firestoreEmulator.spec.ts,downloadableEmulators.start/stopstubbed, real chokidar watcher on a temp file):rulesconfigured → no watcher is created."error"→ does not throw,logLabeledWarning("firestore", …)is called once, message contains the rules path and the error text.stop()closes the watcher.With the source change stashed, test 2 fails with
expected [Function] to not throw an error but 'Error: ENOSPC…' was thrown, i.e. it reproduces the crash this PR fixes.Manual check on Windows 10, Node 24.18.0, Java 21, local build (
node lib/bin/firebase.js):firebase emulators:start --only firestore --project demo-4298withfirebase.jsonpointing atfirestore.rules: emulator starts (All emulators ready).firestore.rules→Change detected, updating rules...thenRules updated.(hot reload path unchanged).firestore.rules:10:1 - ERROR Unexpected 'this'.is printed (change handler still runs).npm run lint(0 errors; the 4 pre-existingjsdoc/no-non-null-assertionwarnings infirestoreEmulator.tsare untouched),npm run format:checkclean,npm run buildsucceeds.Not reproduced end-to-end: an actual inotify failure on a CI image. The failure is simulated by emitting
"error"on the real watcher, which is the same code path chokidar's_handleErroruses.Sample Commands