diff --git a/Microsoft.Azure.Cosmos/src/GatewayStoreClient.cs b/Microsoft.Azure.Cosmos/src/GatewayStoreClient.cs index e151ffcd94..db6f4a027f 100644 --- a/Microsoft.Azure.Cosmos/src/GatewayStoreClient.cs +++ b/Microsoft.Azure.Cosmos/src/GatewayStoreClient.cs @@ -14,7 +14,6 @@ namespace Microsoft.Azure.Cosmos using System.Text; using System.Threading; using System.Threading.Tasks; - using Microsoft.Azure.Cosmos.Handlers; using Microsoft.Azure.Cosmos.Tracing.TraceData; using Microsoft.Azure.Documents; using Microsoft.Azure.Documents.Collections; @@ -132,69 +131,79 @@ internal static INameValueCollection ExtractResponseHeaders(HttpResponseMessage return headers; } + /// + /// Creating a new DocumentClientException using the Gateway response message. + /// + /// + /// internal static async Task CreateDocumentClientExceptionAsync( HttpResponseMessage responseMessage, IClientSideRequestStatistics requestStatistics) { - bool isNameBased = false; - bool isFeed = false; - string resourceTypeString; - string resourceIdOrFullName; - - 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)) + if (string.Equals(responseMessage.Content?.Headers?.ContentType?.MediaType, "application/json", StringComparison.OrdinalIgnoreCase) && + responseMessage.Content?.Headers.ContentLength > 0) { - Stream readStream = await responseMessage.Content.ReadAsStreamAsync(); - Error error = Documents.Resource.LoadFrom(readStream); - return new DocumentClientException( - error, - responseMessage.Headers, - responseMessage.StatusCode) + try + { + Stream contentAsStream = await responseMessage.Content.ReadAsStreamAsync(); + Error error = JsonSerializable.LoadFrom(stream: contentAsStream); + + return new DocumentClientException( + errorResource: error, + responseHeaders: responseMessage.Headers, + statusCode: responseMessage.StatusCode) + { + StatusDescription = responseMessage.ReasonPhrase, + ResourceAddress = resourceIdOrFullName, + RequestStatistics = requestStatistics + }; + } + catch { - StatusDescription = responseMessage.ReasonPhrase, - ResourceAddress = resourceIdOrFullName, - RequestStatistics = requestStatistics - }; + } } - else + + StringBuilder contextBuilder = new StringBuilder(); + contextBuilder.AppendLine(await responseMessage.Content.ReadAsStringAsync()); + + HttpRequestMessage requestMessage = responseMessage.RequestMessage; + + if (requestMessage != null) { - StringBuilder context = new StringBuilder(); - context.AppendLine(await responseMessage.Content.ReadAsStringAsync()); + contextBuilder.AppendLine($"RequestUri: {requestMessage.RequestUri};"); + contextBuilder.AppendLine($"RequestMethod: {requestMessage.Method.Method};"); - HttpRequestMessage requestMessage = responseMessage.RequestMessage; - if (requestMessage != null) + if (requestMessage.Headers != null) { - context.AppendLine($"RequestUri: {requestMessage.RequestUri.ToString()};"); - context.AppendLine($"RequestMethod: {requestMessage.Method.Method};"); - - if (requestMessage.Headers != null) + foreach (KeyValuePair> header in requestMessage.Headers) { - foreach (KeyValuePair> header in requestMessage.Headers) - { - context.AppendLine($"Header: {header.Key} Length: {string.Join(",", header.Value).Length};"); - } + contextBuilder.AppendLine($"Header: {header.Key} Length: {string.Join(",", header.Value).Length};"); } } - - String message = await responseMessage.Content.ReadAsStringAsync(); - return new DocumentClientException( - message: context.ToString(), - innerException: null, - responseHeaders: responseMessage.Headers, - statusCode: responseMessage.StatusCode, - requestUri: responseMessage.RequestMessage.RequestUri) - { - StatusDescription = responseMessage.ReasonPhrase, - ResourceAddress = resourceIdOrFullName, - RequestStatistics = requestStatistics - }; } + + 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 + }; } internal static bool IsAllowedRequestHeader(string headerName) diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GatewayStoreClientTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GatewayStoreClientTests.cs new file mode 100644 index 0000000000..ff750f8ed6 --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GatewayStoreClientTests.cs @@ -0,0 +1,256 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ +namespace Microsoft.Azure.Cosmos +{ + using System; + using System.Net; + using System.Net.Http; + using System.Text; + 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; + + /// + /// Tests for . + /// + [TestClass] + public class GatewayStoreClientTests + { + /// + /// Testing CreateDocumentClientExceptionAsync when media type is NOT application/json and the error message has a length that is not zero. + /// This is not meant to be an exhaustive test for all legitimate content media types. + /// + /// + [TestMethod] + [DataRow("text/html", "")] + [DataRow("text/plain", "This is a test error message.")] + [Owner("philipthomas-MSFT")] + public async Task TestCreateDocumentClientExceptionWhenMediaTypeIsNotApplicationJsonAndErrorMessageLengthIsNotZeroAsync( + string mediaType, + string errorMessage) + { + HttpResponseMessage responseMessage = new(statusCode: HttpStatusCode.NotFound) + { + RequestMessage = new HttpRequestMessage( + method: HttpMethod.Get, + requestUri: @"https://pt_ac_test_uri.com/"), + Content = new StringContent( + mediaType: mediaType, + encoding: Encoding.UTF8, + content: JsonConvert.SerializeObject( + value: new Error() { Code = HttpStatusCode.NotFound.ToString(), Message = errorMessage })), + }; + + DocumentClientException documentClientException = await GatewayStoreClient.CreateDocumentClientExceptionAsync( + responseMessage: responseMessage, + requestStatistics: GatewayStoreClientTests.CreateClientSideRequestStatistics()); + + Assert.IsNotNull(value: documentClientException); + Assert.AreEqual(expected: HttpStatusCode.NotFound, actual: documentClientException.StatusCode); + Assert.IsTrue(condition: documentClientException.Message.Contains(errorMessage)); + + Assert.IsNotNull(value: documentClientException.Error); + Assert.AreEqual(expected: HttpStatusCode.NotFound.ToString(), actual: documentClientException.Error.Code); + Assert.IsTrue(documentClientException.Error.Message.Contains(errorMessage)); + } + + /// + /// Testing CreateDocumentClientExceptionAsync when media type is NOT application/json and the error message has a length that is zero. + /// This is not meant to be an exhaustive test for all legitimate content media types. + /// + /// + [TestMethod] + [DataRow("text/html", "")] + [DataRow("text/html", " ")] + [DataRow("text/plain", "")] + [DataRow("text/plain", " ")] + [Owner("philipthomas-MSFT")] + public async Task TestCreateDocumentClientExceptionWhenMediaTypeIsNotApplicationJsonAndErrorMessageLengthIsZeroAsync( + string mediaType, + string errorMessage) + { + HttpResponseMessage responseMessage = new(statusCode: HttpStatusCode.NotFound) + { + RequestMessage = new HttpRequestMessage( + method: HttpMethod.Get, + requestUri: @"https://pt_ac_test_uri.com/"), + Content = new StringContent( + mediaType: mediaType, + encoding: Encoding.UTF8, + content: JsonConvert.SerializeObject( + value: new Error() { Code = HttpStatusCode.NotFound.ToString(), Message = errorMessage })), + }; + + DocumentClientException documentClientException = await GatewayStoreClient.CreateDocumentClientExceptionAsync( + responseMessage: responseMessage, + requestStatistics: GatewayStoreClientTests.CreateClientSideRequestStatistics()); + + Assert.IsNotNull(value: documentClientException); + Assert.AreEqual(expected: HttpStatusCode.NotFound, actual: documentClientException.StatusCode); + Assert.IsNotNull(value: documentClientException.Message); + + Assert.IsNotNull(value: documentClientException.Error); + Assert.AreEqual(expected: HttpStatusCode.NotFound.ToString(), actual: documentClientException.Error.Code); + Assert.IsNotNull(value: documentClientException.Error.Message); + } + + /// + /// Testing CreateDocumentClientExceptionAsync when media type is NOT application/json and the header content length is zero. + /// This is not meant to be an exhaustive test for all legitimate content media types. + /// + /// + [TestMethod] + [DataRow("text/plain", @"")] + [DataRow("text/plain", @" ")] + [Owner("philipthomas-MSFT")] + public async Task TestCreateDocumentClientExceptionWhenMediaTypeIsNotApplicationJsonAndHeaderContentLengthIsZeroAsync( + string mediaType, + string contentMessage) + { + HttpResponseMessage responseMessage = new(statusCode: HttpStatusCode.NotFound) + { + RequestMessage = new HttpRequestMessage( + method: HttpMethod.Get, + requestUri: @"https://pt_ac_test_uri.com/"), + Content = new StringContent( + mediaType: mediaType, + encoding: Encoding.UTF8, + content: contentMessage), + }; + + DocumentClientException documentClientException = await GatewayStoreClient.CreateDocumentClientExceptionAsync( + responseMessage: responseMessage, + requestStatistics: GatewayStoreClientTests.CreateClientSideRequestStatistics()); + + Assert.IsNotNull(value: documentClientException); + Assert.AreEqual(expected: HttpStatusCode.NotFound, actual: documentClientException.StatusCode); + Assert.IsNotNull(value: documentClientException.Message); + + Assert.IsNotNull(value: documentClientException.Error); + Assert.AreEqual(expected: HttpStatusCode.NotFound.ToString(), actual: documentClientException.Error.Code); + Assert.IsNotNull(value: documentClientException.Error.Message); + } + + /// + /// Testing CreateDocumentClientExceptionAsync when media type is application/json and the error message length is zero. + /// + /// + [TestMethod] + [DataRow("application/json", "")] + [DataRow("application/json", " ")] + [Owner("philipthomas-MSFT")] + public async Task TestCreateDocumentClientExceptionWhenMediaTypeIsApplicationJsonAndErrorMessageLengthIsZeroAsync( + string mediaType, + string errorMessage) + { + HttpResponseMessage responseMessage = new(statusCode: HttpStatusCode.NotFound) + { + RequestMessage = new HttpRequestMessage( + method: HttpMethod.Get, + requestUri: @"https://pt_ac_test_uri.com/"), + Content = new StringContent( + mediaType: mediaType, + encoding: Encoding.UTF8, + content: JsonConvert.SerializeObject( + value: new Error() { Code = HttpStatusCode.NotFound.ToString(), Message = errorMessage })), + }; + + DocumentClientException documentClientException = await GatewayStoreClient.CreateDocumentClientExceptionAsync( + responseMessage: responseMessage, + requestStatistics: GatewayStoreClientTests.CreateClientSideRequestStatistics()); + + Assert.IsNotNull(value: documentClientException); + Assert.AreEqual(expected: HttpStatusCode.NotFound, actual: documentClientException.StatusCode); + Assert.IsNotNull(value: documentClientException.Message); + + Assert.IsNotNull(value: documentClientException.Error); + Assert.AreEqual(expected: HttpStatusCode.NotFound.ToString(), actual: documentClientException.Error.Code); + Assert.IsNotNull(value: documentClientException.Error.Message); + } + + /// + /// Testing CreateDocumentClientExceptionAsync when media type is application/json and the content message is not valid json. + /// and has a content length that is not zero after trim. + /// + /// + [TestMethod] + [DataRow("application/json", @"")] + [DataRow("application/json", @" ")] + [DataRow("application/json", @" ")] + [DataRow("application/json", @" ")] + [DataRow("application/json", @"ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")] + [DataRow("application/json", @" ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890")] + [DataRow("application/json", @"ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 ")] + [DataRow("application/json", @" ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 ")] + [Owner("philipthomas-MSFT")] + public async Task TestCreateDocumentClientExceptionWhenMediaTypeIsApplicationJsonAndContentMessageIsNotValidJsonAsync( + string mediaType, + string contentMessage) + { + HttpResponseMessage responseMessage = new(statusCode: HttpStatusCode.NotFound) + { + RequestMessage = new HttpRequestMessage( + method: HttpMethod.Get, + requestUri: @"https://pt_ac_test_uri.com/"), + Content = new StringContent( + mediaType: mediaType, + encoding: Encoding.UTF8, + content: contentMessage), + }; + + DocumentClientException documentClientException = await GatewayStoreClient.CreateDocumentClientExceptionAsync( + responseMessage: responseMessage, + requestStatistics: GatewayStoreClientTests.CreateClientSideRequestStatistics()); + + Assert.IsNotNull(value: documentClientException); + Assert.AreEqual(expected: HttpStatusCode.NotFound, actual: documentClientException.StatusCode); + Assert.IsTrue(condition: documentClientException.Message.Contains(contentMessage)); + } + + /// + /// Testing CreateDocumentClientExceptionAsync when media type is application/json and the header content length is zero. + /// + [TestMethod] + [DataRow("application/json", @"")] + [DataRow("application/json", @" ")] + [Owner("philipthomas-MSFT")] + public async Task TestCreateDocumentClientExceptionWhenMediaTypeIsApplicationJsonAndHeaderContentLengthIsZeroAsync( + string mediaType, + string contentMessage) + { + HttpResponseMessage responseMessage = new(statusCode: HttpStatusCode.NotFound) + { + RequestMessage = new HttpRequestMessage( + method: HttpMethod.Get, + requestUri: @"https://pt_ac_test_uri.com/"), + Content = new StringContent( + mediaType: mediaType, + encoding: Encoding.UTF8, + content: contentMessage), + }; + + DocumentClientException documentClientException = await GatewayStoreClient.CreateDocumentClientExceptionAsync( + responseMessage: responseMessage, + requestStatistics: GatewayStoreClientTests.CreateClientSideRequestStatistics()); + + Assert.IsNotNull(value: documentClientException); + Assert.AreEqual(expected: HttpStatusCode.NotFound, actual: documentClientException.StatusCode); + Assert.IsNotNull(value: documentClientException.Message); + + Assert.IsNotNull(value: documentClientException.Error); + Assert.AreEqual(expected: HttpStatusCode.NotFound.ToString(), actual: documentClientException.Error.Code); + Assert.IsNotNull(value: documentClientException.Error.Message); + } + + private static IClientSideRequestStatistics CreateClientSideRequestStatistics() + { + return new ClientSideRequestStatisticsTraceDatum( + startTime: DateTime.UtcNow, + trace: NoOpTrace.Singleton); + } + } +}