diff --git a/Microsoft.Azure.Cosmos/src/GatewayStoreClient.cs b/Microsoft.Azure.Cosmos/src/GatewayStoreClient.cs index 9b72682524..1aea6c6ae1 100644 --- a/Microsoft.Azure.Cosmos/src/GatewayStoreClient.cs +++ b/Microsoft.Azure.Cosmos/src/GatewayStoreClient.cs @@ -81,20 +81,16 @@ internal static async Task ParseResponseAsync(HttpRespo { if ((int)responseMessage.StatusCode < 400) { - MemoryStream bufferedStream = new MemoryStream(); - - await responseMessage.Content.CopyToAsync(bufferedStream); - - bufferedStream.Position = 0; - INameValueCollection headers = GatewayStoreClient.ExtractResponseHeaders(responseMessage); - return new DocumentServiceResponse(bufferedStream, headers, responseMessage.StatusCode, serializerSettings); + Stream contentStream = await GatewayStoreClient.BufferContentIfAvailableAsync(responseMessage); + return new DocumentServiceResponse(contentStream, headers, responseMessage.StatusCode, serializerSettings); } else if (request != null && request.IsValidStatusCodeForExceptionlessRetry((int)responseMessage.StatusCode)) { INameValueCollection headers = GatewayStoreClient.ExtractResponseHeaders(responseMessage); - return new DocumentServiceResponse(null, headers, responseMessage.StatusCode, serializerSettings); + Stream contentStream = await GatewayStoreClient.BufferContentIfAvailableAsync(responseMessage); + return new DocumentServiceResponse(contentStream, headers, responseMessage.StatusCode, serializerSettings); } else { @@ -225,6 +221,19 @@ internal static bool IsAllowedRequestHeader(string headerName) return true; } + private static async Task BufferContentIfAvailableAsync(HttpResponseMessage responseMessage) + { + if (responseMessage.Content == null) + { + return null; + } + + MemoryStream bufferedStream = new MemoryStream(); + await responseMessage.Content.CopyToAsync(bufferedStream); + bufferedStream.Position = 0; + return bufferedStream; + } + [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposable object returned by method")] private async Task PrepareRequestMessageAsync( DocumentServiceRequest request, diff --git a/Microsoft.Azure.Cosmos/src/Handler/ResponseMessage.cs b/Microsoft.Azure.Cosmos/src/Handler/ResponseMessage.cs index 60b05795ca..464ca09ddf 100644 --- a/Microsoft.Azure.Cosmos/src/Handler/ResponseMessage.cs +++ b/Microsoft.Azure.Cosmos/src/Handler/ResponseMessage.cs @@ -136,6 +136,7 @@ public virtual ResponseMessage EnsureSuccessStatusCode() { if (!this.IsSuccessStatusCode) { + this.EnsureErrorMessage(); string message = $"Response status code does not indicate success: {(int)this.StatusCode} Substatus: {(int)this.Headers.SubStatusCode} Reason: ({this.ErrorMessage})."; throw new CosmosException( @@ -202,5 +203,44 @@ private void CheckDisposed() throw new ObjectDisposedException(this.GetType().ToString()); } } + + private void EnsureErrorMessage() + { + if (this.Error != null + || !string.IsNullOrEmpty(this.ErrorMessage)) + { + return; + } + + if (this.content != null + && this.content.CanRead) + { + try + { + Error error = Resource.LoadFrom(this.content); + if (error != null) + { + // Error format is not consistent across modes + if (!string.IsNullOrEmpty(error.Message)) + { + this.ErrorMessage = error.Message; + } + else + { + this.ErrorMessage = error.ToString(); + } + } + } + catch (Newtonsoft.Json.JsonReaderException) + { + // Content is not Json + this.content.Position = 0; + using (StreamReader streamReader = new StreamReader(this.content)) + { + this.ErrorMessage = streamReader.ReadToEnd(); + } + } + } + } } } \ No newline at end of file diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosExceptionTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosExceptionTests.cs new file mode 100644 index 0000000000..874be09dc4 --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosExceptionTests.cs @@ -0,0 +1,90 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ + +namespace Microsoft.Azure.Cosmos +{ + using System; + using System.IO; + using System.Net; + using System.Net.Http; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Azure.Cosmos.Common; + using Microsoft.Azure.Cosmos.Routing; + using Microsoft.Azure.Documents; + using Microsoft.VisualStudio.TestTools.UnitTesting; + using Moq; + using Newtonsoft.Json; + + [TestClass] + public class CosmosExceptionTests + { + [TestMethod] + public void EnsureSuccessStatusCode_DontThrowOnSuccess() + { + ResponseMessage responseMessage = new ResponseMessage(HttpStatusCode.OK); + responseMessage.EnsureSuccessStatusCode(); + } + + [TestMethod] + public void EnsureSuccessStatusCode_ThrowsOnFailure() + { + ResponseMessage responseMessage = new ResponseMessage(HttpStatusCode.NotFound); + Assert.ThrowsException(() => responseMessage.EnsureSuccessStatusCode()); + } + + [TestMethod] + public void EnsureSuccessStatusCode_ThrowsOnFailure_ContainsBody() + { + string testContent = "TestContent"; + using (MemoryStream memoryStream = new MemoryStream()) + { + StreamWriter sw = new StreamWriter(memoryStream); + sw.Write(testContent); + sw.Flush(); + memoryStream.Seek(0, SeekOrigin.Begin); + + ResponseMessage responseMessage = new ResponseMessage(HttpStatusCode.NotFound) { Content = memoryStream }; + try + { + responseMessage.EnsureSuccessStatusCode(); + Assert.Fail("Should have thrown"); + } + catch(CosmosException exception) + { + Assert.IsTrue(exception.Message.Contains(testContent)); + } + } + } + + [TestMethod] + public void EnsureSuccessStatusCode_ThrowsOnFailure_ContainsJsonBody() + { + string message = "TestContent"; + Error error = new Error(); + error.Code = "code"; + error.Message = message; + string testContent = JsonConvert.SerializeObject(error); + using (MemoryStream memoryStream = new MemoryStream()) + { + StreamWriter sw = new StreamWriter(memoryStream); + sw.Write(testContent); + sw.Flush(); + memoryStream.Seek(0, SeekOrigin.Begin); + + ResponseMessage responseMessage = new ResponseMessage(HttpStatusCode.NotFound) { Content = memoryStream }; + try + { + responseMessage.EnsureSuccessStatusCode(); + Assert.Fail("Should have thrown"); + } + catch (CosmosException exception) + { + Assert.IsTrue(exception.Message.Contains(message)); + } + } + } + } +} diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GatewayStoreModelTest.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GatewayStoreModelTest.cs index 4268ca8870..ebdc9341ea 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GatewayStoreModelTest.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GatewayStoreModelTest.cs @@ -148,6 +148,58 @@ public async Task TestRetries() } + [TestMethod] + public async Task TestErrorResponsesProvideBody() + { + string testContent = "Content"; + Func> sendFunc = async request => + { + return new HttpResponseMessage(HttpStatusCode.Conflict) { Content = new StringContent(testContent) }; + }; + + Mock mockDocumentClient = new Mock(); + mockDocumentClient.Setup(client => client.ServiceEndpoint).Returns(new Uri("https://foo")); + + GlobalEndpointManager endpointManager = new GlobalEndpointManager(mockDocumentClient.Object, new ConnectionPolicy()); + ISessionContainer sessionContainer = new SessionContainer(string.Empty); + DocumentClientEventSource eventSource = DocumentClientEventSource.Instance; + HttpMessageHandler messageHandler = new MockMessageHandler(sendFunc); + GatewayStoreModel storeModel = new GatewayStoreModel( + endpointManager, + sessionContainer, + TimeSpan.FromSeconds(5), + ConsistencyLevel.Eventual, + eventSource, + null, + new UserAgentContainer(), + ApiType.None, + messageHandler); + + using (new ActivityScope(Guid.NewGuid())) + { + using (DocumentServiceRequest request = + DocumentServiceRequest.Create( + Documents.OperationType.Query, + Documents.ResourceType.Document, + new Uri("https://foo.com/dbs/db1/colls/coll1", UriKind.Absolute), + new MemoryStream(Encoding.UTF8.GetBytes("content1")), + AuthorizationTokenType.PrimaryMasterKey, + null)) + { + request.UseStatusCodeForFailures = true; + request.UseStatusCodeFor429 = true; + + DocumentServiceResponse response = await storeModel.ProcessMessageAsync(request); + Assert.IsNotNull(response.ResponseBody); + using (StreamReader reader = new StreamReader(response.ResponseBody)) + { + Assert.AreEqual(testContent, await reader.ReadToEndAsync()); + } + } + } + + } + /// /// Tests that empty session token is sent for operations on Session Consistent resources like /// Databases, Collections, Users, Permissions, PartitionKeyRanges, DatabaseAccounts and Offers diff --git a/changelog.md b/changelog.md index eb5a5b30cb..bbdf41bb70 100644 --- a/changelog.md +++ b/changelog.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [#726](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/726) Query iterator HasMoreResults now returns false if an exception is hit - [#705](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/705) User agent suffix gets truncated +- [#753](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/753) Reason was not being propagated for Conflict exceptions - [#756](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/756) Change Feed Processor with WithStartTime would execute the delegate the first time with no items. ## [3.1.1](https://www.nuget.org/packages/Microsoft.Azure.Cosmos/3.1.1) - 2019-08-12