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

Error reason not propagating on some status codes #753

Merged
merged 9 commits into from
Sep 4, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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
Expand Down