Skip to content

fix(emulator): keep firebase-admin statics when proxying; don't crash logging circular thrown values - #11460

Closed
ayushflow wants to merge 1 commit into
firebase:mainfrom
ayushflow:fix/emulator-proxy-keep-function-statics
Closed

ayushflow wants to merge 1 commit into
firebase:mainfrom
ayushflow:fix/emulator-proxy-keep-function-statics

Conversation

@ayushflow

Copy link
Copy Markdown

Description

Two Functions emulator bugs, both found while running a firebase-admin 13 project in the emulator. I found no existing issue for either.

1. admin.firestore.Timestamp and admin.firestore.FieldValue are undefined in the Functions emulator (firebase-admin 13).

  • The runtime's Proxied.getOriginal returns value.bind(target) for any function without a prototype.
  • firebase-admin 13 defines admin.firestore as an arrow function with members attached (Timestamp, FieldValue, ...).
  • Function.prototype.bind drops those own properties.

The same code works in production and in plain Node, and fails only under the emulator. In practice, every Firestore write that builds a Timestamp through the namespace throws.

2. A thrown non-Error value with a circular reference crashes the worker. getErrMsg calls JSON.stringify(err), which throws TypeError: Converting circular structure to JSON inside the emulator's rejection handler. The worker dies (socket hang up / ECONNRESET), and the real message is lost.

Fix

  • Proxied.getOriginal still binds, but now copies the function's own property descriptors (except length, name and prototype) onto the bound function, so statics and lazy getters survive.
  • getErrMsg falls back to util.inspect when JSON.stringify throws.
  • CHANGELOG entry added.

Scenarios Tested

Repro (firebase-tools 15.34.2, firebase-admin 13, Node 22):

const admin = require('firebase-admin');
const { onRequest } = require('firebase-functions/v2/https');
admin.initializeApp({ projectId: 'demo-repro' });
exports.probe = onRequest((req, res) =>
  res.json({ Timestamp: typeof admin.firestore.Timestamp, FieldValue: typeof admin.firestore.FieldValue }));
exports.circ = onRequest(async () => { const o = { msg: 'boom' }; o.self = o; throw o; });
  • firebase emulators:start --only functions,firestore, then GET probe: returns {"Timestamp":"undefined","FieldValue":"undefined"} before the fix and "function" for both after it (plain Node also gives "function").
  • GET circ: before the fix, the worker dies with TypeError: Converting circular structure to JSON at getErrMsg. After it, the error is logged and the worker survives.
  • New src/emulator/functionsEmulatorRuntime.spec.ts (Proxied is now exported for it): statics are kept, methods are still bound, and constructors and non-functions are untouched. The statics test fails without the fix (expected undefined to equal [Function: Timestamp]).
  • src/error.spec.ts: getErrMsg/getErrStack on a circular value. It fails without the fix.
  • npx mocha on both spec files: 7 passing. Prettier and tsc --noEmit are clean.

Sample Commands

None. No commands or flags change.

🤖 Generated with Claude Code

…rash logging circular errors

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@google-cla

google-cla Bot commented Nov 6, 2026

Copy link
Copy Markdown

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.

@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 addresses two issues: it preserves static properties of firebase-admin namespaces when functions are bound in the Functions emulator, and it prevents crashes when logging circular references by falling back to util.inspect. The review feedback recommends avoiding the use of 'any' in tests by using Object.assign, skipping 'arguments' and 'caller' properties when copying descriptors to prevent strict mode crashes, and ensuring getErrMsg always returns a string even if JSON.stringify returns undefined.

Comment on lines +9 to +11
const firestore: any = () => "instance";
firestore.Timestamp = class Timestamp {};
firestore.FieldValue = { serverTimestamp: () => "ts" };

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

To adhere to the repository style guide which states 'Never use any or unknown as an escape hatch', we can avoid using any here by using Object.assign to combine the function and its static properties. This allows TypeScript to infer the correct intersection type automatically.

Suggested change
const firestore: any = () => "instance";
firestore.Timestamp = class Timestamp {};
firestore.FieldValue = { serverTimestamp: () => "ts" };
const firestore = Object.assign(() => "instance", {
Timestamp: class Timestamp {},
FieldValue: { serverTimestamp: () => "ts" },
});
References
  1. Never use any or unknown as an escape hatch. Define proper interfaces/types or use type guards. (link)

Comment on lines +123 to +125
if (key === "length" || key === "name" || key === "prototype") {
continue;
}

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

In strict mode, accessing or defining the "arguments" or "caller" properties on functions (especially bound functions, which are exotic objects) can throw a TypeError in some JavaScript engines. To ensure robustness and prevent potential runtime crashes, we should explicitly skip these properties when copying descriptors.

      if (
        key === "length" ||
        key === "name" ||
        key === "prototype" ||
        key === "arguments" ||
        key === "caller"
      ) {
        continue;
      }

Comment thread src/error.ts
Comment on lines +51 to +53
try {
return JSON.stringify(err);
} catch {

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

JSON.stringify(err) can return undefined (not the string "undefined") if err is undefined, a function, or a symbol. Since getErrMsg is typed to return a string, returning undefined violates the type signature and can cause runtime errors in callers expecting a string. Using the nullish coalescing operator with String(err) ensures a string is always returned.

Suggested change
try {
return JSON.stringify(err);
} catch {
try {
return JSON.stringify(err) ?? String(err);
} catch {

@ayushflow ayushflow closed this Nov 6, 2026
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.

1 participant