Database.RunCommand Returns Nil Error when command fails with NoWritesPerformed

XMLWordPrintableJSON

    • Type: Bug
    • Resolution: Fixed
    • Priority: Critical - P2
    • 2.8.1
    • Affects Version/s: None
    • Component/s: Error Handling
    • None
    • None
    • Go Drivers
    • Not Needed
    • None
    • None
    • None
    • None
    • None
    • None

      Summary

       When a command executed via Database.RunCommand fails on its first attempt with a server error that carries the NoWritesPerformed error label, operation.Execute incorrectly sets err = nil and returns the server's
        error-response BSON as the operation result. The caller receives a nil error from SingleResult.Decode, and the raw error BSON is silently decoded into the destination value, corrupting downstream result parsing.

      Detailed steps to reproduce the problem?

        1. Connect to a server that supports the NoWritesPerformed label (e.g., Atlas with rate limiting).
        2. Issue a RunCommand for a write command (e.g., update) that the server rejects with:
              ok: 0
              code: <any>
              errorLabels: ["SystemOverloadedError", "NoWritesPerformed"]
           on the first attempt (no prior retries in scope).
        3. Call Decode(&result) on the returned SingleResult.
        4. Observe: Decode returns nil error; result contains the server's error-response BSON.
        5. Any field lookup on result for success-response fields (e.g., "n", "nModified") returns "element not found".

      Definition of done: what must be done to consider the task complete?

        SingleResult.Decode should return the server error (as a CommandError or equivalent) whenever the server responded with ok: 0, regardless of whether NoWritesPerformed is present. If prevIndefiniteErr is nil when the
        NoWritesPerformed fallback is triggered, the driver should surface the current error (tt) rather than silently succeeding.

      The exact Go version used, with patch level: 1.26.4

      The exact version of the Go driver used: v2.8.0{}

      Describe how MongoDB is set up. Local vs Hosted, version, topology, load balanced, etc.

      Atlas M60 cluster running 9.0.0-rc1018.

      The operating system and version (e.g. Windows 7, OSX 10.8, ...)

      Linux.

      Security Vulnerabilities

      NA.

      Application Impact

      A typical application decodes the RunCommand result and then reads fields from it:

      var result bson.Raw
      err := db.RunCommand(ctx, cmd).Decode(&result)
      if err != nil {
          // handle error — never reached
      }
      // Application assumes result is a valid success response
      n, err := result.LookupErr("n")  // "element not found": the error BSON has no "n" field

       

      Because SingleResult.Decode returns nil, the application proceeds to parse result as if the command succeeded. The server's error-response BSON — containing fields like ok, errmsg, code, codeName, and errorLabels — does not contain the fields the application expects from a successful response. Subsequent LookupErr calls fail with "element not found", or silently return zero values, corrupting result handling.

      The application has no opportunity to inspect the server error (its code, message, or labels)

      because the error was discarded by the driver.

      Possible root cause (AI generated)

      The NoWritesPerformed label is attached by the server to indicate that a failed operation made no durable changes and can be safely retried. The driver uses this label in operation.Execute to select which error to surface when a retry cycle has produced multiple failures — preferring the "indefinite" error from the first attempt over a NoWritesPerformed failure from a subsequent attempt.

      However, this logic seems to misfire when the first and only attempt fails with NoWritesPerformed: there is no prior error to fall back to, so prevIndefiniteErr is nil, and the operation incorrectly appears to have succeeded.

      In mongo/driver/operation.go, inside Execute, the case Error: branch (reached when the server returns ok: 0) contains:

      // Lines 1028–1036
      if tt.HasErrorLabel(NoWritesPerformed) && !prevIndefiniteErrIsSet {
          err = prevIndefiniteErr   // nil on the first attempt — no prior retry has occurred
          prevIndefiniteErrIsSet = true
          goto checkError
      }

       

      prevIndefiniteErr is only populated inside resetForRetry, which is only called when the driver decides to retry. When needRetry is false (e.g., the error lacks the RetryableError label required for adaptive-retry overload retargeting), resetForRetry is never called and prevIndefiniteErr stays nil.

      With err = nil, goto checkError falls into case nil:, which calls ProcessResponseFn with the raw server response and returns nil from Execute:

         case nil:
            if op.ProcessResponseFn != nil {
                perr := op.ProcessResponseFn(ctx, res, info)
                if perr != nil {
                    return perr
                }
            }
            // falls through — returns nil

        For a Command operation, ProcessResponseFn unconditionally stores the response as the result:

       ProcessResponseFn: func(_ context.Context, resp bsoncore.Document, _ driver.ResponseInfo) error {
          c.resultResponse = resp   // stores the ok:0 error BSON as the "success" result
            return nil
      }

       
      Back in Database.RunCommand:

      err = op.Execute(ctx)              // nil — error masked by NoWritesPerformed logic
      rr, convErr := processWriteError(err)  // processWriteError(nil) → (rrAll, nil)
      return &SingleResult{
          err: convErr,                  // nil
          rdr: bson.Raw(op.Result()),    // the server's error-response BSON
      }

      More context

      Failing usage in mongosync (ref)

      if err := dstDB.RunCommand(ctx.WithTimedOp(mscontext.CEADestinationWriteOperation), updateCmd, dstUUID).Decode(&updateRes); err != nil {
        ... snipped (error handling logic) ...
      }// Check whether the update command matched any document.
      nMatch, err := updateRes.LookupErr("n")
      if err != nil {
          raw := bsonext.ShortenForLogging(updateRes, bsonext.DefaultShortLogLength)
          return false, le.NewFromErrorWithMsgf(
              err,
              "failed to parse update result (len=%d): %#q",
              len(updateRes),
              raw,
          )
      }

      Error trace:

       

            Assignee:
            Qingyang Hu
            Reporter:
            Mankawaldeep Singh
            Matt Dale
            Votes:
            0 Vote for this issue
            Watchers:
            3 Start watching this issue

              Created:
              Updated:
              Resolved: