Skip to content

fix(emulator): warn instead of crashing when the Firestore rules watcher fails - #11042

Open
gauranshahuja wants to merge 7 commits into
firebase:mainfrom
gauranshahuja:4298/rules-watcher-fail-gracefully
Open

fix(emulator): warn instead of crashing when the Firestore rules watcher fails#11042
gauranshahuja wants to merge 7 commits into
firebase:mainfrom
gauranshahuja:4298/rules-watcher-fail-gracefully

Conversation

@gauranshahuja

Copy link
Copy Markdown

Description

Addresses #4298.

FirestoreEmulator.start() creates a chokidar watcher for the configured firestore.rules file so that rules can be hot reloaded. The watcher never had an "error" listener. Node's EventEmitter throws when "error" is emitted with no listener, so any watcher failure (the issue reports Error: Unable to watch changes in file "/path/firestore.rules" on CI images without inotify support) took down the whole firebase emulators:start process 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 the CHOKIDAR_USEPOLLING=1 workaround suggested in the issue, then lets the emulator keep running.

What is intentionally not in this PR:

  • The 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.
  • DatabaseEmulator has 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/stop stubbed, real chokidar watcher on a temp file):

  1. No rules configured → no watcher is created.
  2. Watcher emits "error" → does not throw, logLabeledWarning("firestore", …) is called once, message contains the rules path and the error text.
  3. 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-4298 with firebase.json pointing at firestore.rules: emulator starts (All emulators ready).
  • Appending a valid line to firestore.rulesChange detected, updating rules... then Rules updated. (hot reload path unchanged).
  • Appending an invalid line → firestore.rules:10:1 - ERROR Unexpected 'this'. is printed (change handler still runs).

npm run lint (0 errors; the 4 pre-existing jsdoc/no-non-null-assertion warnings in firestoreEmulator.ts are untouched), npm run format:check clean, npm run build succeeds.

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 _handleError uses.

Sample Commands

firebase emulators:start --only firestore --project demo-4298
# then edit firestore.rules while it runs

…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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/emulator/firestoreEmulator.ts Outdated
Comment on lines +50 to +56
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.",
);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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
  1. Never use any or unknown as 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 });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: before rmSync let's do rulesWatcher.close()

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants