bulkWrite returns success items despite errorsOnly:true on write without shard key

    • Type: Bug
    • Resolution: Unresolved
    • Priority: Major - P3
    • None
    • Affects Version/s: None
    • Component/s: None
    • None
    • Query Execution
    • ALL
    • Hide

      Environment

      • MongoDB 8.0.15 (mongos/mongod)
      • Sharded cluster (2 shards, 1 node each), 1 mongos
      • Reproduced with mongosh and Java driver

      1. Set up a cluster

      mlaunch init --sharded 2 --replicaset --nodes 1 --mongos 1

      2. Shard and prep

      use admin;
      sh.enableSharding("test");
      use test;
      db.test.drop();
      db.test.createIndex({ sk: 1 });
      sh.shardCollection("test.test", { sk: 1 });
      sh.splitAt("test.test", { sk: 0 });
      sh.moveChunk("test.test", { sk: MinKey() }, "shard01");
      sh.moveChunk("test.test", { sk: 0 }, "shard02");

      3. Execute bulk write with errorsOnly: true (filter lacks shard key; upsert true)

       db.getSiblingDB('admin').runCommand({
         bulkWrite: 1,
         errorsOnly: true,
         ordered: false,
         nsInfo: [{ ns: "test.test" }],
         ops: [
           { update: 0, multi: false, filter: { _id: 3432542352 }, updateMods: { sk: 0, v: 2 }, upsert: true }
         ]
       }) 

      Expected response:

      {
        cursor: {id: Long('0'), firstBatch: [], ns: 'admin.$cmd.bulkWrite'},
        nErrors: 0,
        nInserted: 0,
        nMatched: 1,
        nModified: 0,
        nUpserted: 1,
        nDeleted: 0,
        retriedStmtIds: [],
        ok: 1,
        '$clusterTime': ......
        operationTime: .....
      } 

      Actual response:

      {
        cursor: {
          id: Long('0'),
          firstBatch: [ { ok: 1, idx: 0, n: 1, upserted: { _id: 3432542352 } } ],
          ns: 'admin.$cmd.bulkWrite'
        },
        nErrors: 0,
        nInserted: 0,
        nMatched: 1,
        nModified: 0,
        nUpserted: 1,
        nDeleted: 0,
        retriedStmtIds: [],
        ok: 1,   '$clusterTime':
         ......   operationTime: ..... 
      } 

      Control case (when filter includes shard key to perform single-phase write, errorsOnly is honored)

      db.getSiblingDB('admin').runCommand({
         bulkWrite: 1,
         errorsOnly: true,
         ordered: false,
         nsInfo: [{ ns: "test.test" }],
         ops: [
           { update: 0, multi: false, filter: { sk: 6 }, updateMods: { sk: 0, v: 2 }, upsert: true }
         ]
       }) 
      Show
      Environment MongoDB 8.0.15 (mongos/mongod) Sharded cluster (2 shards, 1 node each), 1 mongos Reproduced with mongosh and Java driver 1 . Set up a cluster mlaunch init --sharded 2 --replicaset --nodes 1 --mongos 1 2. Shard and prep use admin; sh.enableSharding( "test" ); use test; db.test.drop(); db.test.createIndex({ sk: 1 }); sh.shardCollection( "test.test" , { sk: 1 }); sh.splitAt( "test.test" , { sk: 0 }); sh.moveChunk( "test.test" , { sk: MinKey() }, "shard01" ); sh.moveChunk( "test.test" , { sk: 0 }, "shard02" ); 3. Execute bulk write with errorsOnly: true (filter lacks shard key; upsert true) db.getSiblingDB( 'admin' ).runCommand({   bulkWrite: 1,   errorsOnly: true ,   ordered: false ,   nsInfo: [{ ns: "test.test" }],   ops: [     { update: 0, multi: false , filter: { _id: 3432542352 }, updateMods: { sk: 0, v: 2 }, upsert: true }   ] }) Expected response: {   cursor: {id: Long ( '0' ), firstBatch: [], ns: 'admin.$cmd.bulkWrite' },   nErrors: 0,   nInserted: 0,   nMatched: 1,   nModified: 0,   nUpserted: 1,   nDeleted: 0,   retriedStmtIds: [],   ok: 1,   '$clusterTime' : ......   operationTime: ..... } Actual response: {   cursor: {     id: Long ( '0' ),     firstBatch: [ { ok: 1, idx: 0, n: 1, upserted: { _id: 3432542352 } } ],     ns: 'admin.$cmd.bulkWrite'   },   nErrors: 0,   nInserted: 0,   nMatched: 1,   nModified: 0,   nUpserted: 1,   nDeleted: 0,   retriedStmtIds: [],   ok: 1,   '$clusterTime' : ......   operationTime: ..... } Control case (when filter includes shard key to perform single-phase write, errorsOnly is honored) db.getSiblingDB( 'admin' ).runCommand({   bulkWrite: 1,   errorsOnly: true ,   ordered: false ,   nsInfo: [{ ns: "test.test" }],   ops: [     { update: 0, multi: false , filter: { sk: 6 }, updateMods: { sk: 0, v: 2 }, upsert: true }   ] })
    • None
    • None
    • None
    • None
    • None
    • None
    • None

      When issuing a bulkWrite with errorsOnly:true and the operation goes through the two-phase write (write_ops/write_without_shard_key_util.cpp) without shard key path (because the filter doesn’t contain the shard key and the write requires an upsert), the server returns a cursor containing per-item success reply items. This violates the contract of errorsOnly:true, which should suppress non-error per-item replies.

      This was observed on the two-phase/no-shard-key upsert path (mongos orchestrated). Single-phase writes (targetable to a single shard), and unsharded collections, honor errorsOnly:true (no success items returned).

      Impact:

      • Client libraries receive unexpected reply items, increasing network/CPU overhead.
      • Drivers that assume errorsOnly:true suppresses success items (e.g., Java driver) assert/fail on the unexpected cursor contents, surfacing exceptions to applications despite successful writes.

      Root cause analysys:

      The response comes from write_without_shard_key::runTwoPhaseWriteProtocol (line 615), which internally:

      1. When it performs an upsert, calls constructUpsertResponse at write_without_shard_key_util.cpp#L366-L372.
      2. constructUpsertResponse always builds a full response with all the details for BulkWrite operations, regardless of whether getErrorsOnly is set

      Then when this response is parsed in bulk_write_exec.cpp, it's used as-is without filtering.

      In contrast, when handling empty responses in bulk_write_exec.cpp (lines 628-634), the code checks getErrorsOnly:

        if (!bulkWriteOp.getClientRequest().getErrorsOnly()) {
            items.push_back(BulkWriteReplyItem(0));
        }

            Assignee:
            Unassigned
            Reporter:
            Slav Babanin
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated: