From 48c6108d4a7960b7dea0bbaf5addda03e1215f10 Mon Sep 17 00:00:00 2001 From: Nalu Tripician <27316859+NaluTripician@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:34:21 -0800 Subject: [PATCH] [Internal] Batch: Fixes null ErrorMessage when promoting status from MultiStatus response When a batch response returns 207 MultiStatus and the SDK promotes the status code from a failing operation (e.g. 409 Conflict), also promote the error message from the failing operation's ResourceStream. Previously only StatusCode and SubStatusCode were promoted, leaving ErrorMessage as null since the outer 207 response has no error message. Fixes #5649 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/Batch/TransactionalBatchResponse.cs | 13 ++++- .../Batch/BatchSchemaTests.cs | 54 ++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/Microsoft.Azure.Cosmos/src/Batch/TransactionalBatchResponse.cs b/Microsoft.Azure.Cosmos/src/Batch/TransactionalBatchResponse.cs index ba7a1d473c..6fa0b1606f 100644 --- a/Microsoft.Azure.Cosmos/src/Batch/TransactionalBatchResponse.cs +++ b/Microsoft.Azure.Cosmos/src/Batch/TransactionalBatchResponse.cs @@ -361,6 +361,7 @@ private static async Task PopulateFromContentAsync( HttpStatusCode responseStatusCode = responseMessage.StatusCode; SubStatusCodes responseSubStatusCode = responseMessage.Headers.SubStatusCode; + string responseErrorMessage = responseMessage.ErrorMessage; // Promote the operation error status as the Batch response error status if we have a MultiStatus response // to provide users with status codes they are used to. @@ -373,6 +374,16 @@ private static async Task PopulateFromContentAsync( { responseStatusCode = result.StatusCode; responseSubStatusCode = result.SubStatusCode; + + if (result.ResourceStream != null) + { + using (StreamReader reader = new StreamReader(result.ResourceStream, encoding: System.Text.Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true)) + { + responseErrorMessage = reader.ReadToEnd(); + result.ResourceStream.Position = 0; + } + } + break; } } @@ -381,7 +392,7 @@ private static async Task PopulateFromContentAsync( TransactionalBatchResponse response = new TransactionalBatchResponse( responseStatusCode, responseSubStatusCode, - responseMessage.ErrorMessage, + responseErrorMessage, responseMessage.Headers, trace, serverRequest.Operations, diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Batch/BatchSchemaTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Batch/BatchSchemaTests.cs index f419974da9..6806756948 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Batch/BatchSchemaTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Batch/BatchSchemaTests.cs @@ -177,7 +177,59 @@ public async Task BatchResponseDeserializationAsync() Assert.IsTrue(comparer.Equals(results[1], batchResponse[1])); } - private class ItemBatchOperationEqualityComparer : IEqualityComparer + [TestMethod] + [Owner("nalutripician")] + public async Task BatchResponseDeserializationPromotesErrorMessageAsync() + { + string expectedErrorMessage = "{\"Errors\":[\"Resource with specified id or name already exists.\"]}"; + byte[] errorBody = System.Text.Encoding.UTF8.GetBytes(expectedErrorMessage); + + using CosmosClient cosmosClient = MockCosmosUtil.CreateMockCosmosClient(); + ContainerInternal containerCore = (ContainerInlineCore)cosmosClient.GetDatabase("db").GetContainer("cont"); + List results = new List + { + new TransactionalBatchOperationResult(HttpStatusCode.Conflict) + { + ResourceStream = new CloneableStream( + internalStream: new MemoryStream(errorBody, index: 0, count: errorBody.Length, writable: false, publiclyVisible: true), + allowUnsafeDataAccess: true), + }, + new TransactionalBatchOperationResult(HttpStatusCode.FailedDependency) + }; + + MemoryStream responseContent = await new BatchResponsePayloadWriter(results).GeneratePayloadAsync(); + + SinglePartitionKeyServerBatchRequest batchRequest = await SinglePartitionKeyServerBatchRequest.CreateAsync( + partitionKey: Cosmos.PartitionKey.None, + operations: new ArraySegment( + new ItemBatchOperation[] + { + new ItemBatchOperation(OperationType.Create, operationIndex: 0, id: "someId", containerCore: containerCore), + new ItemBatchOperation(OperationType.Create, operationIndex: 1, id: "someId2", containerCore: containerCore) + }), + serializerCore: MockCosmosUtil.Serializer, + trace: NoOpTrace.Singleton, + cancellationToken: CancellationToken.None); + + ResponseMessage response = new ResponseMessage((HttpStatusCode)StatusCodes.MultiStatus) { Content = responseContent }; + response.Headers.Session = Guid.NewGuid().ToString(); + response.Headers.ActivityId = Guid.NewGuid().ToString(); + + TransactionalBatchResponse batchResponse = await TransactionalBatchResponse.FromResponseMessageAsync( + response, + batchRequest, + MockCosmosUtil.Serializer, + true, + NoOpTrace.Singleton, + CancellationToken.None); + + Assert.IsNotNull(batchResponse); + Assert.AreEqual(HttpStatusCode.Conflict, batchResponse.StatusCode); + Assert.IsNotNull(batchResponse.ErrorMessage, "ErrorMessage should be promoted from the failing operation's ResourceStream"); + Assert.AreEqual(expectedErrorMessage, batchResponse.ErrorMessage); + } + + private class ItemBatchOperationEqualityComparer: IEqualityComparer { public bool Equals(ItemBatchOperation x, ItemBatchOperation y) {