Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GatewayClientStore: Fixes an issue with dealing with invalid JSON HTTP responses #4229

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.
Jump to
Jump to file
Failed to load files.
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 INameValueCollection ExtractResponseHeaders(HttpResponseMessage
// 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))
philipthomas-MSFT marked this conversation as resolved.
Show resolved Hide resolved
{
error = new Error
{
Code = responseMessage.StatusCode.ToString(),
Message = "No response content from gateway."
ealsur marked this conversation as resolved.
Show resolved Hide resolved
};
}
else
{
Stream readStream = await responseMessage.Content.ReadAsStreamAsync();
error = Documents.Resource.LoadFrom<Error>(readStream);
}

philipthomas-MSFT marked this conversation as resolved.
Show resolved Hide resolved
return new DocumentClientException(
error,
responseMessage.Headers,
Expand Down Expand Up @@ -197,6 +214,29 @@ internal static INameValueCollection ExtractResponseHeaders(HttpResponseMessage
}
}

/// <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(@"")]
philipthomas-MSFT marked this conversation as resolved.
Show resolved Hide resolved
[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."));
philipthomas-MSFT marked this conversation as resolved.
Show resolved Hide resolved

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);
}
}
}