Skip to content

Commit

Permalink
Error reason not propagating on some status codes (#753)
Browse files Browse the repository at this point in the history
* Gateway should return body if available

* Ensure error message is present

* Test to validate gateway response

* Json support

* EnsureSuccessStatus tests

* Extra lines

* changelog
  • Loading branch information
ealsur authored and kirankumarkolli committed Sep 4, 2019
1 parent b101e6c commit c5038a9
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 8 deletions.
25 changes: 17 additions & 8 deletions Microsoft.Azure.Cosmos/src/GatewayStoreClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,20 +81,16 @@ internal static async Task<DocumentServiceResponse> 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
{
Expand Down Expand Up @@ -225,6 +221,19 @@ internal static bool IsAllowedRequestHeader(string headerName)
return true;
}

private static async Task<Stream> 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<HttpRequestMessage> PrepareRequestMessageAsync(
DocumentServiceRequest request,
Expand Down
40 changes: 40 additions & 0 deletions Microsoft.Azure.Cosmos/src/Handler/ResponseMessage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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<Error>(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();
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<CosmosException>(() => 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));
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,58 @@ public async Task TestRetries()

}

[TestMethod]
public async Task TestErrorResponsesProvideBody()
{
string testContent = "Content";
Func<HttpRequestMessage, Task<HttpResponseMessage>> sendFunc = async request =>
{
return new HttpResponseMessage(HttpStatusCode.Conflict) { Content = new StringContent(testContent) };
};

Mock<IDocumentClientInternal> mockDocumentClient = new Mock<IDocumentClientInternal>();
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());
}
}
}

}

/// <summary>
/// Tests that empty session token is sent for operations on Session Consistent resources like
/// Databases, Collections, Users, Permissions, PartitionKeyRanges, DatabaseAccounts and Offers
Expand Down
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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
Expand Down

0 comments on commit c5038a9

Please sign in to comment.