BSON.calculateObjectSize hangs indefinitely on an object containing a circular reference

XMLWordPrintableJSON

    • Type: Bug
    • Resolution: Unresolved
    • Priority: Major - P3
    • None
    • Affects Version/s: None
    • Component/s: BSON
    • 1
    • None
    • None
    • None
    • None
    • None
    • None
    • None

      Summary

      BSON.calculateObjectSize (and by extension BSON.serialize) hangs indefinitely on an object containing a circular reference, instead of throwing an error. This blocks the Node.js event loop entirely, since the call is synchronous — there is no way for calling code (timeouts, Promise.race, etc.) to recover once triggered.

      Environment

      • Package: bson
      • Version: 7.3.1
      • Node.js: v22.13.0 / v24.13.0 (reproduced on both)
      • OS: macOS (Apple Silicon) and Linux (Ubuntu 22.04, x86_64) — reproduced on both

      Steps to Reproduce

      import { BSON } from 'bson';
      
      const circular = { name: 'test' };
      circular.self = circular;
      
      console.log('calling BSON.calculateObjectSize...');
      const size = BSON.calculateObjectSize(circular); // never returns
      console.log('size:', size);
      

      Expected Behavior

      The call should throw a clear, immediate error identifying the circular reference — matching the behavior of JSON.stringify on the same object:

      TypeError: Converting circular structure to JSON
          --> starting at object with constructor 'Object'
          --- property 'self' closes the circle
      

      Actual Behavior

      BSON.calculateObjectSize never returns and never throws. The process spins at ~100% CPU indefinitely (confirmed via a live V8 CPU profile and debugger stack trace attached to the hung process — the call stack sits in utf8ByteLengthcalculateElementSizeinternalCalculateObjectSizecalculateObjectSize on continuous repeat). Since this happens synchronously, no JS-level timeout (test framework timeouts, Promise.race guards, etc.) can ever fire to recover — the only way out is an external process kill.

      Cross-Implementation Comparison

      The equivalent operation in PyMongo's bson package (C extension, the default fast path) fails immediately and cleanly on the identical structure:

      import json
      from bson import encode
      
      circular = {'name': 'test'}
      circular['self'] = circular
      
      json.dumps(circular)   # ValueError: Circular reference detected (~0.0000s)
      encode(circular)       # RecursionError: maximum recursion depth exceeded while encoding an object to BSON (~0.0016s)
      

      Notably, PyMongo doesn't appear to have deliberate cycle detection either — its encoder is implemented via straightforward recursion, so Python's own stack-depth limit acts as an accidental backstop and fails fast. The JS bson implementation's traversal apparently avoids blowing the JS call stack (likely iterative rather than naively recursive), so it never hits an equivalent safety net and just spins forever instead.

      Real-World Impact / How This Was Found

      Discovered via mastra-ai/mastra's shared vector-store test suite (stores/_test-utils/src/domains/vector/error-handling.ts), which has a test named "should handle metadata with circular references" intended to verify that a vector store either serializes or cleanly rejects circular metadata. For the MongoDB store (@mastra/mongodb), the test passes a circular object as vector metadata to upsert(), which is handed to the driver's bulkWrite() — which calls calculateObjectSize while building the write, hanging the entire test process (and, in CI, hanging until the external task-level timeout killed the host, up to 60 minutes, with zero diagnostic output since the event loop was fully blocked).

      Suggested Fix

      Track visited object references during traversal (e.g. a Set/WeakSet of ancestor objects, matching the approach JSON.stringify uses internally) and throw a clear "circular reference" error immediately upon detecting a cycle, rather than relying on an unbounded traversal.

      Additional Notes

      • This may also affect other MongoDB drivers with generically-serialized, reference-capable document types (candidates worth checking: Ruby, Java, C#/.NET, Go) — not yet empirically verified, flagged here as a follow-up.
      • Filed while investigating CI hangs for a new mastra-js integration in mongodb-labs/ai-ml-pipeline-testing (tracking ticket: NODE-7617). We've worked around it there by skipping the affected test with a patch and a comment referencing this ticket.

            Assignee:
            Unassigned
            Reporter:
            Casey Clements
            None
            Votes:
            0 Vote for this issue
            Watchers:
            4 Start watching this issue

              Created:
              Updated: