Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 18 additions & 12 deletions Microsoft.Azure.Cosmos/src/GatewayStoreClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -168,31 +168,37 @@ internal static async Task<DocumentClientException> 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<Error>(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<Error>(stream: contentStream);

return new DocumentClientException(
errorResource: error,
responseHeaders: responseMessage.Headers,
statusCode: responseMessage.StatusCode)
{
StatusDescription = responseMessage.ReasonPhrase,
ResourceAddress = resourceIdOrFullName,
RequestStatistics = requestStatistics
};
}
}
catch
{
}
}

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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,38 @@ public async Task TestCreateDocumentClientExceptionWhenMediaTypeIsApplicationJso
Assert.IsNotNull(value: documentClientException.Error.Message);
}

/// <summary>
/// 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.
/// </summary>
[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(
Expand Down
Loading