-
Type:
Bug
-
Resolution: Fixed
-
Priority:
Critical - P2
-
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:
- fixes
-
GODRIVER-4098 `FindOneAndUpdate` returns `ErrNoDocuments` instead of real server error.
-
- Closed
-
- is depended on by
-
TOOLS-4331 Ensure mongorestore adaptive retry works under --retryWrites=false
-
- In Code Review
-
-
TOOLS-4329 Bump Go driver from v2.5.0 to >v2.8.0 (to support retrying on server load shedding)
-
- Waiting (Blocked)
-