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 10 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
204 changes: 190 additions & 14 deletions Microsoft.Azure.Cosmos/src/GatewayStoreClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ namespace Microsoft.Azure.Cosmos
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Net;
philipthomas-MSFT marked this conversation as resolved.
Show resolved Hide resolved
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
Expand All @@ -19,13 +22,15 @@ namespace Microsoft.Azure.Cosmos
using Microsoft.Azure.Documents;
using Microsoft.Azure.Documents.Collections;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

internal class GatewayStoreClient : TransportClient
{
private readonly ICommunicationEventSource eventSource;
private readonly CosmosHttpClient httpClient;
private readonly JsonSerializerSettings SerializerSettings;
private static readonly HttpMethod httpPatchMethod = new HttpMethod(HttpConstants.HttpMethods.Patch);
private static readonly string NoResponseContentFromGateway = "No response content from gateway.";

public GatewayStoreClient(
CosmosHttpClient httpClient,
Expand Down Expand Up @@ -132,26 +137,196 @@ internal static INameValueCollection ExtractResponseHeaders(HttpResponseMessage
return headers;
}

/// <summary>
/// Creating a new DocumentClientException using the Gateway response message.
/// </summary>
/// <param name="responseMessage"></param>
/// <param name="requestStatistics"></param>
internal static async Task<DocumentClientException> CreateDocumentClientExceptionAsync(
HttpResponseMessage responseMessage,
IClientSideRequestStatistics requestStatistics)
{
bool isNameBased = false;
bool isFeed = false;
string resourceTypeString;
string resourceIdOrFullName;
if (responseMessage is null)
{
throw new ArgumentNullException(nameof(responseMessage));
}

if (requestStatistics is null)
{
throw new ArgumentNullException(nameof(requestStatistics));
}

string resourceLink = responseMessage.RequestMessage.RequestUri.LocalPath;
if (!PathsHelper.TryParsePathSegments(resourceLink, out isFeed, out resourceTypeString, out resourceIdOrFullName, out isNameBased))
if (!PathsHelper.TryParsePathSegments(
resourceUrl: responseMessage.RequestMessage.RequestUri.LocalPath,
isFeed: out _,
resourcePath: out _,
resourceIdOrFullName: out string resourceIdOrFullName,
isNameBased: out _))
{
// if resourceLink is invalid - we will not set resourceAddress in exception.
}

// 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))
try
{
Stream readStream = await responseMessage.Content.ReadAsStreamAsync();
Error error = Documents.Resource.LoadFrom<Error>(readStream);
Stream contentAsStream = await responseMessage.Content.ReadAsStreamAsync();
Error error = GatewayStoreClient.GetError(
philipthomas-MSFT marked this conversation as resolved.
Show resolved Hide resolved
error: Documents.Resource.LoadFrom<Error>(stream: contentAsStream),
statusCode: responseMessage.StatusCode);

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(GatewayStoreClient.GetError(
error: await responseMessage.Content.ReadAsStringAsync(),
statusCode: responseMessage.StatusCode));

HttpRequestMessage requestMessage = responseMessage.RequestMessage;

if (requestMessage != null)
{
contextBuilder.AppendLine($"RequestUri: {requestMessage.RequestUri};");
contextBuilder.AppendLine($"RequestMethod: {requestMessage.Method.Method};");

if (requestMessage.Headers != null)
{
foreach (KeyValuePair<string, IEnumerable<string>> header in requestMessage.Headers)
{
contextBuilder.AppendLine($"Header: {header.Key} Length: {string.Join(",", header.Value).Length};");
}
}
}

return new DocumentClientException(
message: contextBuilder.ToString(),
innerException: null,
responseHeaders: responseMessage.Headers,
statusCode: responseMessage.StatusCode,
requestUri: responseMessage.RequestMessage.RequestUri)
{
StatusDescription = responseMessage.ReasonPhrase,
ResourceAddress = resourceIdOrFullName,
RequestStatistics = requestStatistics
};
}
}

/// <summary>
/// Get or create an Error type using an existing Error type.
/// </summary>
/// <param name="error"></param>
/// <param name="statusCode"></param>
private static Error GetError(
Error error,
HttpStatusCode statusCode)
{
if (error.Message.Trim().Length == 0)
{
return new Error
{
Code = statusCode.ToString(),
Message = GatewayStoreClient.NoResponseContentFromGateway,
};
}

return error;
}

/// <summary>
/// Get or set an Error string using an existing Error string.
/// </summary>
/// <param name="error"></param>
/// <param name="statusCode"></param>
private static string GetError(
string error,
HttpStatusCode statusCode)
{
if (error.Trim().Length == 0)
{
return JsonConvert.SerializeObject(
new Error
{
Code = statusCode.ToString(),
Message = GatewayStoreClient.NoResponseContentFromGateway,
});
}

return error;
}

/// <summary>
/// Creating a new DocumentClientException using the Gateway response message.
/// </summary>
/// <param name="responseMessage"></param>
/// <param name="requestStatistics"></param>
internal static async Task<DocumentClientException> WorkingCreateDocumentClientExceptionAsync(
philipthomas-MSFT marked this conversation as resolved.
Show resolved Hide resolved
HttpResponseMessage responseMessage,
IClientSideRequestStatistics requestStatistics)
{
if (responseMessage is null)
{
throw new ArgumentNullException(nameof(responseMessage));
}

if (requestStatistics is null)
{
throw new ArgumentNullException(nameof(requestStatistics));
}

// Ask, what is the purpose of this, really?
// The only impact of try parse fail is an empty resourceIdOrFullName.

if (!PathsHelper.TryParsePathSegments(
resourceUrl: responseMessage.RequestMessage.RequestUri.LocalPath,
isFeed: out _,
resourcePath: out _,
resourceIdOrFullName: out string resourceIdOrFullName,
isNameBased: out _))
{
// if resourceLink is invalid - we will not set resourceAddress in exception.
}

Error error;

try
{
await Console.Out.WriteLineAsync($"ContentLength: {responseMessage.Content?.Headers?.ContentLength}");

if (!string.Equals(responseMessage.Content?.Headers?.ContentType?.MediaType, "application/json", StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentNullException();
}

// Check ContentLength on Header and Length of trimmed Message's Content.

string messageContent = await responseMessage.Content.ReadAsStringAsync();
int messageContentLength = messageContent.Trim().Length;
long? headerContentLength = responseMessage.Content?.Headers?.ContentLength;

if (headerContentLength == 0 || messageContentLength == 0)
{
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);

if (error.Message.Trim().Length == 0)
{
error.Message = "No response content from gateway.";
}
}

philipthomas-MSFT marked this conversation as resolved.
Show resolved Hide resolved
return new DocumentClientException(
error,
responseMessage.Headers,
Expand All @@ -162,15 +337,17 @@ internal static INameValueCollection ExtractResponseHeaders(HttpResponseMessage
RequestStatistics = requestStatistics
};
}
else
catch
{
StringBuilder context = new StringBuilder();
context.AppendLine(await responseMessage.Content.ReadAsStringAsync());
string readString = await responseMessage.Content.ReadAsStringAsync();

context.AppendLine(readString);

HttpRequestMessage requestMessage = responseMessage.RequestMessage;
if (requestMessage != null)
{
context.AppendLine($"RequestUri: {requestMessage.RequestUri.ToString()};");
context.AppendLine($"RequestUri: {requestMessage.RequestUri};");
context.AppendLine($"RequestMethod: {requestMessage.Method.Method};");

if (requestMessage.Headers != null)
Expand All @@ -182,7 +359,6 @@ internal static INameValueCollection ExtractResponseHeaders(HttpResponseMessage
}
}

String message = await responseMessage.Content.ReadAsStringAsync();
philipthomas-MSFT marked this conversation as resolved.
Show resolved Hide resolved
return new DocumentClientException(
message: context.ToString(),
innerException: null,
Expand Down