Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
44 changes: 42 additions & 2 deletions Microsoft.Azure.Cosmos/src/GatewayStoreClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ namespace Microsoft.Azure.Cosmos
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
Expand All @@ -19,6 +20,7 @@ namespace Microsoft.Azure.Cosmos
using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Collections;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

internal class GatewayStoreClient : TransportClient
{
Expand Down Expand Up @@ -150,8 +152,23 @@ 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.
if (string.Equals(responseMessage.Content?.Headers?.ContentType?.MediaType, "application/json", StringComparison.OrdinalIgnoreCase))
{
Stream readStream = await responseMessage.Content.ReadAsStreamAsync();
Error error = Documents.Resource.LoadFrom<Error>(readStream);
// For more information, see https://github.com/Azure/azure-cosmos-dotnet-v3/issues/4162.
Error error;

if (await GatewayStoreClient.IsJsonHTTPResponseFromGatewayInvalidAsync(responseMessage))
{
error = new Error
{
Code = responseMessage.StatusCode.ToString(),
Message = "No response content from gateway."
};
}
else
{
Stream readStream = await responseMessage.Content.ReadAsStreamAsync();
error = Documents.Resource.LoadFrom<Error>(readStream);
}

return new DocumentClientException(
error,
responseMessage.Headers,
Expand Down Expand Up @@ -197,6 +214,29 @@ internal static async Task<DocumentClientException> CreateDocumentClientExceptio
}
}

/// <summary>
/// Checking if exception response (deserializable Error object) is valid based on the content length.
/// For more information, see <see href="https://github.com/Azure/azure-cosmos-dotnet-v3/issues/4162."/>.
/// </summary>
/// <param name="responseMessage"></param>
private static async Task<bool> IsJsonHTTPResponseFromGatewayInvalidAsync(HttpResponseMessage responseMessage)
{
string readString = await responseMessage.Content.ReadAsStringAsync();

try
{
_ = JToken.Parse(readString);

return responseMessage.Content?.Headers?.ContentLength == 0 ||
readString.Trim().Length == 0;
}
catch (JsonReaderException)
{
return true;
}

}

internal static bool IsAllowedRequestHeader(string headerName)
{
if (!headerName.StartsWith("x-ms", StringComparison.OrdinalIgnoreCase))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
//------------------------------------------------------------
namespace Microsoft.Azure.Cosmos
{
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Azure.Cosmos.Tracing;
using Microsoft.Azure.Cosmos.Tracing.TraceData;
using Microsoft.Azure.Documents;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;

/// <summary>
/// Tests for <see cref="GatewayStoreClient"/>.
/// </summary>
[TestClass]
public class GatewayStoreClientTests
{
/// <summary>
/// Testing the exception behavior when a response from the Gateway has no response (deserializable Error object) based on the content length.
/// For more information, see <see href="https://github.com/Azure/azure-cosmos-dotnet-v3/issues/4162"/>.
/// </summary>
/// <returns></returns>
[TestMethod]
[DataRow(@"")]
[DataRow(@" ")]
[DataRow(@"<!DOCTYPE html><html><body></body></html>")]
[DataRow(@" <!DOCTYPE html><html><body></body></html>")]
[DataRow(@"<!DOCTYPE html><html><body></body></html> ")]
[DataRow(@" <!DOCTYPE html><html><body></body></html> ")]
[DataRow(@"ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")]
[DataRow(@" ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")]
[DataRow(@"ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 ")]
[DataRow(@" ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 ")]
public async Task CreateDocumentClientExceptionInvalidJsonResponseFromGatewayTestAsync(string content)
{
HttpResponseMessage responseMessage = new(statusCode: System.Net.HttpStatusCode.NotFound)
{
RequestMessage = new HttpRequestMessage(
method: HttpMethod.Get,
requestUri: @"https://pt_ac_test_uri.com/"),
Content = new StringContent(
content: content),
};

responseMessage.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");

IClientSideRequestStatistics requestStatistics = new ClientSideRequestStatisticsTraceDatum(
startTime: DateTime.UtcNow,
trace: NoOpTrace.Singleton);

DocumentClientException documentClientException = await GatewayStoreClient.CreateDocumentClientExceptionAsync(
responseMessage: responseMessage,
requestStatistics: requestStatistics);

Assert.IsNotNull(value: documentClientException);
Assert.AreEqual(expected: HttpStatusCode.NotFound, actual: documentClientException.StatusCode);
Assert.IsTrue(condition: documentClientException.Message.Contains("No response content from gateway."));

Assert.IsNotNull(value: documentClientException.Error);
Assert.AreEqual(expected: HttpStatusCode.NotFound.ToString(), actual: documentClientException.Error.Code);
Assert.AreEqual(expected: "No response content from gateway.", actual: documentClientException.Error.Message);
}

/// <summary>
/// Testing the exception behavior when a response from the Gateway has a response (deserializable Error object) based the content length.
/// For more information, see <see href="https://github.com/Azure/azure-cosmos-dotnet-v3/issues/4162"/>.
/// </summary>
/// <returns></returns>
[TestMethod]
[DataRow(@"This is the content of a test error message.")]
public async Task CreateDocumentClientExceptionValidJsonResponseFromGatewayTestAsync(string content)
{
HttpResponseMessage responseMessage = new(statusCode: System.Net.HttpStatusCode.NotFound)
{
RequestMessage = new HttpRequestMessage(
method: HttpMethod.Get,
requestUri: @"https://pt_ac_test_uri.com/"),
Content = new StringContent(
content: JsonConvert.SerializeObject(
value: new Error() { Code = HttpStatusCode.NotFound.ToString(), Message = content })),
};

responseMessage.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");

IClientSideRequestStatistics requestStatistics = new ClientSideRequestStatisticsTraceDatum(
startTime: DateTime.UtcNow,
trace: NoOpTrace.Singleton);

DocumentClientException documentClientException = await GatewayStoreClient.CreateDocumentClientExceptionAsync(
responseMessage: responseMessage,
requestStatistics: requestStatistics);

Assert.IsNotNull(value: documentClientException);
Assert.AreEqual(expected: HttpStatusCode.NotFound, actual: documentClientException.StatusCode);
Assert.IsTrue(condition: documentClientException.Message.Contains(content));

Assert.IsNotNull(value: documentClientException.Error);
Assert.AreEqual(expected: HttpStatusCode.NotFound.ToString(), actual: documentClientException.Error.Code);
Assert.AreEqual(expected: content, actual: documentClientException.Error.Message);
}
}
}