Skip to content

mongodb find().toArray(): an inherited static (MongoDBResponse.make) called through a subclass reads its module-scope bindings wrongly (value is not a function) #11200

Description

@proggeramlug

mongodb 7.5.0's collection.find({}).toArray() fails with TypeError: value is not a function. This is the next CRUD-fixture blocker after #11187 (find().sort()) and #11157. The failing call is CursorResponse.make(bson). make is a static method inherited from MongoDBResponse, and it uses module-scope bindings. When it is called through the subclass, those bindings read wrong values. The instrumented fixture showed typeof isErrorResponse === "object" when make ran as CursorResponse.make and "function" when it ran as MongoDBResponse.make.

It reproduces on origin/main 25ef463 without any package. It also reproduces with PR #11199 (#11187) and PR #11188 applied.

Package-free repro (two CommonJS files + entry)

rel/doc.js:

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Doc = void 0;
class Doc {
    constructor(bson, offset = 0, isArray = false, elements) { this.cache = Object.create(null); this.bson = bson; this.elements = elements ?? [bson.length]; }
    get(name) { return this.cache[name] ?? null; }
}
exports.Doc = Doc;

rel/resp.js (reduced by greedy line removal from a mirror of mongodb/lib/cmap/wire_protocol/responses.js):

"use strict";
const lib_1 = { parse(b) { return [b.length]; } };
const doc_1 = require("./doc");
const Off = { a: 0, b: 1 };
function isErr(b, els) { for (let i = 0; i < els.length; i++) { if (els[i] === Off.b + 100) return true; } return b === "err"; }
class Base extends doc_1.Doc {
    static make(bson) {
        const elements = (0, lib_1.parse)(bson);
        const isError = isErr(bson, elements);
        return isError ? new Base(bson, 0, false, elements) : new this(bson, 0, false, elements);
    }
}
exports.Base = Base;
class Sub extends Base {
    constructor() { super(...arguments); this._batch = null; this.iterated = 0; }
    get id() { try { return lib_1.parse(this.cursor); } catch (cause) { throw new err_1.E(cause.message); } }
}
exports.Sub = Sub;
class Sub2 extends Sub {
}

main.ts:

const r = require("./rel/resp.js");
for (const [l, f] of [["Base.make", () => r.Base.make("abc").constructor.name], ["Sub.make", () => r.Sub.make("abc").constructor.name]] as any[]) {
  try { console.log(l, f()); } catch (e: any) { console.log(l, "THREW", e.message); }
}

Node 26.5.1:

Base.make Base
Sub.make Sub

Perry (main 25ef463, perry-dev, Linux x64, PERRY_NO_AUTO_OPTIMIZE=1):

Base.make Base
Sub.make THREW value is not a function

The reducer kept every one of these ingredients:

  • Sub's explicit constructor() { super(...arguments); ... }.
  • A getter on Sub.
  • A further subclass Sub2 extends Sub.
  • A cross-module base, Base extends doc_1.Doc.
  • The module-scope bindings that make reads: lib_1, isErr, and Off, which isErr reads.

Remove any one of them and the call succeeds. new Sub(...) called directly works, and so does Base.make. Only the inherited static that is invoked with this = Sub fails.

In mongodb

Fixture: mongodb 7.5.0 + bson 7.3.3, auto-optimize on, perry.compilePackages = mongodb and its deps. connect, deleteMany and insertMany succeed. Then await collection.find({}).toArray() fails, with or without .sort(). The throw originates in Connection.sendWire, at (responseType ?? MongoDBResponse).make(bson) with responseType = CursorResponse. Instrumentation inside MongoDBResponse.make printed:

make this MongoDBResponse ... typeof isErrorResponse = function   (insert/delete: fine)
make this CursorResponse  ... typeof isErrorResponse = object     (find: throws calling it)

Possibly the same family as #10911 (a class's capturing static methods read captures as undefined). Here the captures come back as the wrong values, and only when the static is reached through a subclass.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions