Skip to content

fix(NODE-6878): documents.clear() throws a TypeError after cursor is rewound #4488

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Apr 18, 2025
9 changes: 4 additions & 5 deletions src/cmap/wire_protocol/responses.ts
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ import {
parseToElementsToArray,
parseUtf8ValidationOption,
pluckBSONSerializeOptions,
serialize,
type Timestamp
} from '../../bson';
import { MONGODB_ERROR_CODES, MongoUnexpectedServerResponseError } from '../../error';
@@ -230,11 +231,9 @@ export class CursorResponse extends MongoDBResponse {
* This supports a feature of the FindCursor.
* It is an optimization to avoid an extra getMore when the limit has been reached
*/
static emptyGetMore: CursorResponse = {
id: new Long(0),
length: 0,
shift: () => null
} as unknown as CursorResponse;
static get emptyGetMore(): CursorResponse {
return new CursorResponse(serialize({ ok: 1, cursor: { id: 0n, nextBatch: [] } }));
}

static override is(value: unknown): value is CursorResponse {
return value instanceof CursorResponse || value === CursorResponse.emptyGetMore;
30 changes: 29 additions & 1 deletion test/integration/crud/find.test.js
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ const { assert: test } = require('../shared');
const { expect } = require('chai');
const sinon = require('sinon');
const { setTimeout } = require('timers');
const { Code, ObjectId, Long, Binary, ReturnDocument } = require('../../mongodb');
const { Code, ObjectId, Long, Binary, ReturnDocument, CursorResponse } = require('../../mongodb');

describe('Find', function () {
let client;
@@ -2388,4 +2388,32 @@ describe('Find', function () {
});
});
});

it(
'regression test (NODE-6878): CursorResponse.emptyGetMore contains all CursorResponse fields',
{ requires: { topology: 'sharded' } },
async function () {
const collection = client.db('rewind-regression').collection('bar');

await collection.deleteMany({});
await collection.insertMany(Array.from({ length: 4 }, (_, i) => ({ x: i })));

const getMoreSpy = sinon.spy(CursorResponse, 'emptyGetMore', ['get']);

const cursor = collection.find({}, { batchSize: 1, limit: 3 });
// emptyGetMore is used internally after limit + 1 documents have been iterated
await cursor.next();
await cursor.next();
await cursor.next();
await cursor.next();

// assert that `emptyGetMore` is called. if it is not, this test
// always passes, even without the fix in NODE-6878.
expect(getMoreSpy.get).to.have.been.called;

cursor.rewind();

await cursor.toArray();
}
);
});