diff --git a/Microsoft.Azure.Cosmos/src/GatewayStoreClient.cs b/Microsoft.Azure.Cosmos/src/GatewayStoreClient.cs index 44603e8a43..5701316ac6 100644 --- a/Microsoft.Azure.Cosmos/src/GatewayStoreClient.cs +++ b/Microsoft.Azure.Cosmos/src/GatewayStoreClient.cs @@ -168,23 +168,28 @@ internal static async Task CreateDocumentClientExceptio } // If service rejects the initial payload like header is to large it will return an HTML error instead of JSON. + string contentString = null; if (string.Equals(responseMessage.Content?.Headers?.ContentType?.MediaType, "application/json", StringComparison.OrdinalIgnoreCase) && responseMessage.Content?.Headers.ContentLength > 0) { try { - Stream contentAsStream = await responseMessage.Content.ReadAsStreamAsync(); - Error error = JsonSerializable.LoadFrom(stream: contentAsStream); - - return new DocumentClientException( - errorResource: error, - responseHeaders: responseMessage.Headers, - statusCode: responseMessage.StatusCode) + // Buffer the content once to avoid "stream already consumed" issue + contentString = await responseMessage.Content.ReadAsStringAsync(); + using (MemoryStream contentStream = new MemoryStream(Encoding.UTF8.GetBytes(contentString))) { - StatusDescription = responseMessage.ReasonPhrase, - ResourceAddress = resourceIdOrFullName, - RequestStatistics = requestStatistics - }; + Error error = JsonSerializable.LoadFrom(stream: contentStream); + + return new DocumentClientException( + errorResource: error, + responseHeaders: responseMessage.Headers, + statusCode: responseMessage.StatusCode) + { + StatusDescription = responseMessage.ReasonPhrase, + ResourceAddress = resourceIdOrFullName, + RequestStatistics = requestStatistics + }; + } } catch { @@ -192,7 +197,8 @@ internal static async Task CreateDocumentClientExceptio } StringBuilder contextBuilder = new StringBuilder(); - contextBuilder.AppendLine(await responseMessage.Content.ReadAsStringAsync()); + // Reuse the already buffered content if available, otherwise read it now + contextBuilder.AppendLine(contentString ?? await responseMessage.Content.ReadAsStringAsync()); HttpRequestMessage requestMessage = responseMessage.RequestMessage; diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GatewayStoreClientTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GatewayStoreClientTests.cs index 98a54c40f2..8da257bc5c 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GatewayStoreClientTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GatewayStoreClientTests.cs @@ -246,6 +246,38 @@ public async Task TestCreateDocumentClientExceptionWhenMediaTypeIsApplicationJso Assert.IsNotNull(value: documentClientException.Error.Message); } + /// + /// Test to verify the fix for the stream consumption issue when JSON deserialization fails. + /// This reproduces the scenario where a 403 response has application/json content type + /// but invalid JSON content, which would previously cause "stream already consumed" exception. + /// Fixes issue #5243. + /// + [TestMethod] + [Owner("copilot")] + public async Task TestStreamConsumptionBugFixWhenJsonDeserializationFails() + { + // Create invalid JSON content that will fail deserialization but has application/json content type + string invalidJson = "{ \"error\": invalid json content that will fail parsing }"; + + HttpResponseMessage responseMessage = new HttpResponseMessage(HttpStatusCode.Forbidden) + { + RequestMessage = new HttpRequestMessage(HttpMethod.Get, "https://test.com/dbs/db1/colls/coll1/docs/doc1"), + Content = new StringContent(invalidJson, Encoding.UTF8, "application/json") + }; + + IClientSideRequestStatistics requestStatistics = GatewayStoreClientTests.CreateClientSideRequestStatistics(); + + // This should NOT throw an InvalidOperationException about stream being consumed + DocumentClientException exception = await GatewayStoreClient.CreateDocumentClientExceptionAsync( + responseMessage: responseMessage, + requestStatistics: requestStatistics); + + // Verify the exception was created successfully with fallback logic + Assert.IsNotNull(exception); + Assert.AreEqual(HttpStatusCode.Forbidden, exception.StatusCode); + Assert.IsTrue(exception.Message.Contains(invalidJson), "Exception message should contain the original invalid JSON content"); + } + private static IClientSideRequestStatistics CreateClientSideRequestStatistics() { return new ClientSideRequestStatisticsTraceDatum(