From 1a999ffb2d81a969f99b0c3d93e82f7a7127b37c Mon Sep 17 00:00:00 2001 From: j82w Date: Fri, 28 Feb 2020 06:53:22 -0800 Subject: [PATCH] Added stack trace to CosmosException, ResponseMessage.ErrorMessage includes full exception info. (#1213) * Removed Error since it is not being used anywhere. * Removed error since it's not being used * Refactored added new exception types * ResponseMessage.ErrorMessage will now return the full CosmosException string. Returning only the error message makes it not possible to debug. CosmosException now stores the stack trace. This fixes the issues where the error information is stored and later thrown causing the exception to show the incorrect error location. Created new CosmosExceptionFactory. This helps produce a CosmosException from the various types. * Fixed build issue * Renamed exception to add Cosmos to avoid confusion with Document name space versions. * Fixed exception handling and updated tests. * Fixed tests * Fixed ExceptionWithStackTraceException to use original exception stack trace. * Fixed merge conflicts with latest * Added diagnostic context to exceptions. * Fixed test for retail build * Updated error message field in linq tests * Converted the stack trace to a string. Removed creating the stack trace for exception less path. * Updated changelog * Removed typed exceptions to avoid exposing internal types. * Adding transport client exception tests. * Update Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosExceptionFactory.cs Co-Authored-By: Matias Quaranta * Update Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosExceptionFactory.cs Co-Authored-By: Matias Quaranta * Removed diagnostics from Response.ErrorMessage * Fixed unit test * Adding Error object to CosmosException for back compatability. * Adding unit test for Error handling Co-authored-by: Matias Quaranta --- .../TransactionalBatchOperationResult.cs | 3 +- .../src/Encryption/EncryptionProcessor.cs | 3 +- .../src/Handler/ResponseMessage.cs | 78 +--- ...smosCrossPartitionQueryExecutionContext.cs | 7 +- .../ItemProducers/ItemProducer.cs | 4 +- .../CosmosOrderByItemQueryExecutionContext.cs | 7 +- .../Core/QueryClient/QueryResponseCore.cs | 12 +- .../Core/QueryPlan/QueryPlanRetriever.cs | 12 +- .../src/Query/Core/QueryResponseFactory.cs | 87 +++- .../Query/v3Query/CosmosQueryClientCore.cs | 2 +- .../src/Query/v3Query/QueryIterator.cs | 3 +- .../src/Query/v3Query/QueryResponse.cs | 15 +- .../src/Resource/ClientContextCore.cs | 15 +- .../src/Resource/Container/ContainerCore.cs | 5 +- .../CosmosExceptions/BadRequestException.cs | 27 -- .../{ => CosmosExceptions}/CosmosException.cs | 148 ++++--- .../CosmosExceptionFactory.cs | 372 ++++++++++++++++++ .../CosmosExceptions/CosmosHttpException.cs | 27 -- .../InternalServerErrorException.cs | 27 -- .../DataEncryptionKeyCore.cs | 14 +- .../src/Resource/Offer/CosmosOffers.cs | 4 +- Microsoft.Azure.Cosmos/src/Util/Extensions.cs | 142 ++++--- .../BaselineTest/BaselineTests.cs | 4 +- ...ralBaselineTests.TestThenByTranslation.xml | 4 +- .../ChangeFeed/SmokeTests.cs | 1 + .../CosmosContainerTests.cs | 1 + .../CosmosDiagnosticsTests.cs | 51 ++- .../CrossPartitionQueryTests.cs | 3 +- .../Utils/TransportClientHelper.cs | 91 +++++ .../ChangeFeedResultSetIteratorTests.cs | 13 +- .../CosmosExceptionTests.cs | 130 ++++++ .../CosmosQueryUnitTests.cs | 23 +- .../DotNetSDKAPI.json | 22 +- .../GlobalEndpointManagerTest.cs | 11 +- .../Query/ItemProducerTreeUnitTests.cs | 4 +- .../Query/QueryPipelineMockTests.cs | 4 +- .../Query/QueryResponseFactoryTests.cs | 49 ++- .../Query/QueryResponseMessageFactory.cs | 21 +- changelog.md | 3 + 39 files changed, 1086 insertions(+), 363 deletions(-) delete mode 100644 Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/BadRequestException.cs rename Microsoft.Azure.Cosmos/src/Resource/{ => CosmosExceptions}/CosmosException.cs (66%) create mode 100644 Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosExceptionFactory.cs delete mode 100644 Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosHttpException.cs delete mode 100644 Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/InternalServerErrorException.cs create mode 100644 Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Utils/TransportClientHelper.cs diff --git a/Microsoft.Azure.Cosmos/src/Batch/TransactionalBatchOperationResult.cs b/Microsoft.Azure.Cosmos/src/Batch/TransactionalBatchOperationResult.cs index c9d311b227..7631d2cfb4 100644 --- a/Microsoft.Azure.Cosmos/src/Batch/TransactionalBatchOperationResult.cs +++ b/Microsoft.Azure.Cosmos/src/Batch/TransactionalBatchOperationResult.cs @@ -212,9 +212,8 @@ internal ResponseMessage ToResponseMessage() ResponseMessage responseMessage = new ResponseMessage( statusCode: this.StatusCode, requestMessage: null, - errorMessage: null, - error: null, headers: headers, + cosmosException: null, diagnostics: this.DiagnosticsContext ?? CosmosDiagnosticsContext.Create()) { Content = this.ResourceStream diff --git a/Microsoft.Azure.Cosmos/src/Encryption/EncryptionProcessor.cs b/Microsoft.Azure.Cosmos/src/Encryption/EncryptionProcessor.cs index 31b74e612f..da39bddbd0 100644 --- a/Microsoft.Azure.Cosmos/src/Encryption/EncryptionProcessor.cs +++ b/Microsoft.Azure.Cosmos/src/Encryption/EncryptionProcessor.cs @@ -11,6 +11,7 @@ namespace Microsoft.Azure.Cosmos using System.Text; using System.Threading; using System.Threading.Tasks; + using Microsoft.Azure.Cosmos.Resource.CosmosExceptions; using Microsoft.Azure.Documents; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -134,7 +135,7 @@ public async Task DecryptAsync( EncryptionProperties encryptionProperties = encryptionPropertiesJObj.ToObject(); if (encryptionProperties.EncryptionFormatVersion != 1) { - throw new CosmosException(HttpStatusCode.InternalServerError, $"Unknown encryption format version: {encryptionProperties.EncryptionFormatVersion}. Please upgrade your SDK to the latest version."); + throw CosmosExceptionFactory.CreateInternalServerErrorException($"Unknown encryption format version: {encryptionProperties.EncryptionFormatVersion}. Please upgrade your SDK to the latest version."); } DataEncryptionKeyCore tempDek = (DataEncryptionKeyInlineCore)database.GetDataEncryptionKey(id: "unknown"); diff --git a/Microsoft.Azure.Cosmos/src/Handler/ResponseMessage.cs b/Microsoft.Azure.Cosmos/src/Handler/ResponseMessage.cs index 847b8481c7..e8985efc23 100644 --- a/Microsoft.Azure.Cosmos/src/Handler/ResponseMessage.cs +++ b/Microsoft.Azure.Cosmos/src/Handler/ResponseMessage.cs @@ -8,6 +8,7 @@ namespace Microsoft.Azure.Cosmos using System.Diagnostics; using System.IO; using System.Net; + using Microsoft.Azure.Cosmos.Resource.CosmosExceptions; using Microsoft.Azure.Documents; /// @@ -22,6 +23,7 @@ public ResponseMessage() { this.Headers = new Headers(); this.DiagnosticsContext = CosmosDiagnosticsContext.Create(); + this.CosmosException = null; } /// @@ -42,9 +44,16 @@ public ResponseMessage( this.StatusCode = statusCode; this.RequestMessage = requestMessage; - this.ErrorMessage = errorMessage; this.Headers = new Headers(); this.DiagnosticsContext = requestMessage?.DiagnosticsContext ?? CosmosDiagnosticsContext.Create(); + + if (!string.IsNullOrEmpty(errorMessage)) + { + this.CosmosException = CosmosExceptionFactory.Create( + statusCode, + requestMessage, + errorMessage); + } } /// @@ -52,22 +61,19 @@ public ResponseMessage( /// /// The HttpStatusCode of the response /// The object - /// The reason for failures if any. - /// The inner error object /// The headers for the response. + /// The exception if the response is from an error. /// The diagnostics for the request internal ResponseMessage( HttpStatusCode statusCode, RequestMessage requestMessage, - string errorMessage, - Error error, Headers headers, + CosmosException cosmosException, CosmosDiagnosticsContext diagnostics) { this.StatusCode = statusCode; this.RequestMessage = requestMessage; - this.ErrorMessage = errorMessage; - this.Error = error; + this.CosmosException = cosmosException; this.Headers = headers ?? new Headers(); this.DiagnosticsContext = diagnostics ?? throw new ArgumentNullException(nameof(diagnostics)); } @@ -93,7 +99,7 @@ public virtual Stream Content /// /// Gets the reason for a failure in the current response. /// - public virtual string ErrorMessage { get; internal set; } + public virtual string ErrorMessage => this.CosmosException?.ToString(includeDiagnostics: false); /// /// Gets the current HTTP headers. @@ -120,10 +126,7 @@ public virtual Stream Content internal CosmosDiagnosticsContext DiagnosticsContext { get; } - /// - /// Gets the internal error object. - /// - internal virtual Error Error { get; set; } + internal CosmosException CosmosException { get; } private bool disposed; @@ -132,24 +135,18 @@ public virtual Stream Content /// /// Asserts if the current is a success. /// - public virtual bool IsSuccessStatusCode => ((int)this.StatusCode >= 200) && ((int)this.StatusCode <= 299); + public virtual bool IsSuccessStatusCode => this.StatusCode.IsSuccess(); /// /// Checks if the current has a successful status code, otherwise, throws. /// - /// An instance of representing the error state. + /// An instance of representing the error state. /// The current . 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( - this, - message, - this.Error); + throw CosmosExceptionFactory.Create(this); } return this; @@ -210,44 +207,5 @@ 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 = Documents.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/src/Query/Core/ExecutionContext/CosmosCrossPartitionQueryExecutionContext.cs b/Microsoft.Azure.Cosmos/src/Query/Core/ExecutionContext/CosmosCrossPartitionQueryExecutionContext.cs index b279395834..2bb1056365 100644 --- a/Microsoft.Azure.Cosmos/src/Query/Core/ExecutionContext/CosmosCrossPartitionQueryExecutionContext.cs +++ b/Microsoft.Azure.Cosmos/src/Query/Core/ExecutionContext/CosmosCrossPartitionQueryExecutionContext.cs @@ -401,12 +401,7 @@ protected async Task TryInitializeAsync( if (failureResponse.HasValue) { return TryCatch.FromException( - new CosmosException( - statusCode: failureResponse.Value.StatusCode, - subStatusCode: (int)failureResponse.Value.SubStatusCode.GetValueOrDefault(0), - message: failureResponse.Value.ErrorMessage, - activityId: failureResponse.Value.ActivityId, - requestCharge: failureResponse.Value.RequestCharge)); + failureResponse.Value.CosmosException); } if (!movedToNextPage) diff --git a/Microsoft.Azure.Cosmos/src/Query/Core/ExecutionContext/ItemProducers/ItemProducer.cs b/Microsoft.Azure.Cosmos/src/Query/Core/ExecutionContext/ItemProducers/ItemProducer.cs index b9bdbcb48a..0008c30e3e 100644 --- a/Microsoft.Azure.Cosmos/src/Query/Core/ExecutionContext/ItemProducers/ItemProducer.cs +++ b/Microsoft.Azure.Cosmos/src/Query/Core/ExecutionContext/ItemProducers/ItemProducer.cs @@ -14,6 +14,8 @@ namespace Microsoft.Azure.Cosmos.Query.Core.ExecutionContext.ItemProducers using Microsoft.Azure.Cosmos.Query.Core.Collections; using Microsoft.Azure.Cosmos.Query.Core.Metrics; using Microsoft.Azure.Cosmos.Query.Core.QueryClient; + using Microsoft.Azure.Cosmos.Resource.CosmosExceptions; + using Microsoft.Azure.Documents; using PartitionKeyRange = Documents.PartitionKeyRange; using PartitionKeyRangeIdentity = Documents.PartitionKeyRangeIdentity; @@ -289,7 +291,7 @@ public async Task BufferMoreDocumentsAsync(CancellationToken token) feedResponse = QueryResponseCore.CreateFailure( statusCode: (System.Net.HttpStatusCode)429, subStatusCodes: null, - errorMessage: "Request Rate Too Large", + cosmosException: CosmosExceptionFactory.CreateThrottledException("Request Rate Too Large"), requestCharge: 0, activityId: QueryResponseCore.EmptyGuidString, diagnostics: QueryResponseCore.EmptyDiagnostics); diff --git a/Microsoft.Azure.Cosmos/src/Query/Core/ExecutionContext/OrderBy/CosmosOrderByItemQueryExecutionContext.cs b/Microsoft.Azure.Cosmos/src/Query/Core/ExecutionContext/OrderBy/CosmosOrderByItemQueryExecutionContext.cs index b705e575be..64d859262f 100644 --- a/Microsoft.Azure.Cosmos/src/Query/Core/ExecutionContext/OrderBy/CosmosOrderByItemQueryExecutionContext.cs +++ b/Microsoft.Azure.Cosmos/src/Query/Core/ExecutionContext/OrderBy/CosmosOrderByItemQueryExecutionContext.cs @@ -641,12 +641,7 @@ private async Task TryFilterAsync( if (failureResponse.HasValue) { return TryCatch.FromException( - new CosmosException( - statusCode: failureResponse.Value.StatusCode, - subStatusCode: (int)failureResponse.Value.SubStatusCode.GetValueOrDefault(0), - message: failureResponse.Value.ErrorMessage, - activityId: failureResponse.Value.ActivityId, - requestCharge: 0)); + failureResponse.Value.CosmosException); } break; diff --git a/Microsoft.Azure.Cosmos/src/Query/Core/QueryClient/QueryResponseCore.cs b/Microsoft.Azure.Cosmos/src/Query/Core/QueryClient/QueryResponseCore.cs index 061153859f..368c79b432 100644 --- a/Microsoft.Azure.Cosmos/src/Query/Core/QueryClient/QueryResponseCore.cs +++ b/Microsoft.Azure.Cosmos/src/Query/Core/QueryClient/QueryResponseCore.cs @@ -35,7 +35,7 @@ private QueryResponseCore( long responseLengthBytes, string disallowContinuationTokenMessage, string continuationToken, - string errorMessage, + CosmosException cosmosException, SubStatusCodes? subStatusCode) { this.IsSuccess = isSuccess; @@ -47,13 +47,13 @@ private QueryResponseCore( this.RequestCharge = requestCharge; this.DisallowContinuationTokenMessage = disallowContinuationTokenMessage; this.ContinuationToken = continuationToken; - this.ErrorMessage = errorMessage; + this.CosmosException = cosmosException; this.SubStatusCode = subStatusCode; } internal IReadOnlyList CosmosElements { get; } - internal string ErrorMessage { get; } + internal CosmosException CosmosException { get; } internal SubStatusCodes? SubStatusCode { get; } @@ -92,7 +92,7 @@ internal static QueryResponseCore CreateSuccess( responseLengthBytes: responseLengthBytes, disallowContinuationTokenMessage: disallowContinuationTokenMessage, continuationToken: continuationToken, - errorMessage: null, + cosmosException: null, subStatusCode: null); return cosmosQueryResponse; @@ -101,7 +101,7 @@ internal static QueryResponseCore CreateSuccess( internal static QueryResponseCore CreateFailure( HttpStatusCode statusCode, SubStatusCodes? subStatusCodes, - string errorMessage, + CosmosException cosmosException, double requestCharge, string activityId, IReadOnlyCollection diagnostics) @@ -116,7 +116,7 @@ internal static QueryResponseCore CreateFailure( responseLengthBytes: 0, disallowContinuationTokenMessage: null, continuationToken: null, - errorMessage: errorMessage, + cosmosException: cosmosException, subStatusCode: subStatusCodes); return cosmosQueryResponse; diff --git a/Microsoft.Azure.Cosmos/src/Query/Core/QueryPlan/QueryPlanRetriever.cs b/Microsoft.Azure.Cosmos/src/Query/Core/QueryPlan/QueryPlanRetriever.cs index a5b8a29827..8d56169b29 100644 --- a/Microsoft.Azure.Cosmos/src/Query/Core/QueryPlan/QueryPlanRetriever.cs +++ b/Microsoft.Azure.Cosmos/src/Query/Core/QueryPlan/QueryPlanRetriever.cs @@ -9,6 +9,7 @@ namespace Microsoft.Azure.Cosmos.Query.Core.QueryPlan using System.Threading.Tasks; using Microsoft.Azure.Cosmos.Query.Core.Monads; using Microsoft.Azure.Cosmos.Query.Core.QueryClient; + using Microsoft.Azure.Cosmos.Resource.CosmosExceptions; using OperationType = Documents.OperationType; using PartitionKeyDefinition = Documents.PartitionKeyDefinition; using ResourceType = Documents.ResourceType; @@ -62,9 +63,14 @@ public static async Task GetQueryPlanWithServiceI if (!tryGetQueryPlan.Succeeded) { - throw new CosmosException( - System.Net.HttpStatusCode.BadRequest, - tryGetQueryPlan.Exception.ToString()); + if (tryGetQueryPlan.Exception is CosmosException) + { + throw tryGetQueryPlan.Exception; + } + + throw CosmosExceptionFactory.CreateBadRequestException( + message: tryGetQueryPlan.Exception.ToString(), + stackTrace: tryGetQueryPlan.Exception.StackTrace); } return tryGetQueryPlan.Result; diff --git a/Microsoft.Azure.Cosmos/src/Query/Core/QueryResponseFactory.cs b/Microsoft.Azure.Cosmos/src/Query/Core/QueryResponseFactory.cs index 5859b1dfd5..2342e38f17 100644 --- a/Microsoft.Azure.Cosmos/src/Query/Core/QueryResponseFactory.cs +++ b/Microsoft.Azure.Cosmos/src/Query/Core/QueryResponseFactory.cs @@ -5,10 +5,13 @@ namespace Microsoft.Azure.Cosmos.Query.Core { using System; + using System.Diagnostics; + using System.Runtime.CompilerServices; using Microsoft.Azure.Cosmos.Query.Core.Exceptions; using Microsoft.Azure.Cosmos.Query.Core.Monads; using Microsoft.Azure.Cosmos.Query.Core.QueryClient; using Microsoft.Azure.Cosmos.Resource.CosmosExceptions; + using Microsoft.Azure.Documents; internal static class QueryResponseFactory { @@ -32,14 +35,7 @@ public static QueryResponseCore CreateFromException(Exception exception) } else if (exception is ExceptionWithStackTraceException exceptionWithStackTrace) { - QueryResponseCore innerExceptionResponse = QueryResponseFactory.CreateFromException(exceptionWithStackTrace.InnerException); - queryResponseCore = QueryResponseCore.CreateFailure( - statusCode: innerExceptionResponse.StatusCode, - subStatusCodes: innerExceptionResponse.SubStatusCode, - errorMessage: exceptionWithStackTrace.ToString(), - requestCharge: innerExceptionResponse.RequestCharge, - activityId: innerExceptionResponse.ActivityId, - diagnostics: innerExceptionResponse.Diagnostics); + queryResponseCore = QueryResponseFactory.CreateFromExceptionWithStackTrace(exceptionWithStackTrace); } else { @@ -50,11 +46,22 @@ public static QueryResponseCore CreateFromException(Exception exception) } else { + CosmosException unkownCosmosException = CosmosExceptionFactory.CreateInternalServerErrorException( + subStatusCode: default, + message: exception.Message, + stackTrace: exception.StackTrace, + activityId: QueryResponseCore.EmptyGuidString, + requestCharge: 0, + retryAfter: null, + headers: null, + diagnosticsContext: null, + innerException: exception); + // Unknown exception type should become a 500 queryResponseCore = QueryResponseCore.CreateFailure( statusCode: System.Net.HttpStatusCode.InternalServerError, subStatusCodes: null, - errorMessage: exception?.ToString(), + cosmosException: unkownCosmosException, requestCharge: 0, activityId: QueryResponseCore.EmptyGuidString, diagnostics: QueryResponseCore.EmptyDiagnostics); @@ -69,7 +76,7 @@ private static QueryResponseCore CreateFromCosmosException(CosmosException cosmo QueryResponseCore queryResponseCore = QueryResponseCore.CreateFailure( statusCode: cosmosException.StatusCode, subStatusCodes: (Microsoft.Azure.Documents.SubStatusCodes)cosmosException.SubStatusCode, - errorMessage: cosmosException.ToString(), + cosmosException: cosmosException, requestCharge: 0, activityId: cosmosException.ActivityId, diagnostics: QueryResponseCore.EmptyDiagnostics); @@ -79,10 +86,14 @@ private static QueryResponseCore CreateFromCosmosException(CosmosException cosmo private static QueryResponseCore CreateFromDocumentClientException(Microsoft.Azure.Documents.DocumentClientException documentClientException) { + CosmosException cosmosException = CosmosExceptionFactory.Create( + documentClientException, + null); + QueryResponseCore queryResponseCore = QueryResponseCore.CreateFailure( statusCode: documentClientException.StatusCode.GetValueOrDefault(System.Net.HttpStatusCode.InternalServerError), subStatusCodes: null, - errorMessage: documentClientException.ToString(), + cosmosException: cosmosException, requestCharge: 0, activityId: documentClientException.ActivityId, diagnostics: QueryResponseCore.EmptyDiagnostics); @@ -90,6 +101,40 @@ private static QueryResponseCore CreateFromDocumentClientException(Microsoft.Azu return queryResponseCore; } + private static QueryResponseCore CreateFromExceptionWithStackTrace(ExceptionWithStackTraceException exceptionWithStackTrace) + { + // Use the original stack trace from the inner exception. + if (exceptionWithStackTrace.InnerException is DocumentClientException + || exceptionWithStackTrace.InnerException is CosmosException) + { + return QueryResponseFactory.CreateFromException(exceptionWithStackTrace.InnerException); + } + + QueryResponseCore queryResponseCore = QueryResponseFactory.CreateFromException(exceptionWithStackTrace.InnerException); + CosmosException cosmosException = queryResponseCore.CosmosException; + + queryResponseCore = QueryResponseCore.CreateFailure( + statusCode: queryResponseCore.StatusCode, + subStatusCodes: queryResponseCore.SubStatusCode, + cosmosException: CosmosExceptionFactory.Create( + cosmosException.StatusCode, + cosmosException.SubStatusCode, + cosmosException.Message, + exceptionWithStackTrace.StackTrace, + cosmosException.ActivityId, + cosmosException.RequestCharge, + cosmosException.RetryAfter, + cosmosException.Headers, + cosmosException.DiagnosticsContext, + cosmosException.Error, + cosmosException.InnerException), + requestCharge: queryResponseCore.RequestCharge, + activityId: queryResponseCore.ActivityId, + diagnostics: queryResponseCore.Diagnostics); + + return queryResponseCore; + } + private sealed class QueryExceptionConverter : QueryExceptionVisitor { public static readonly QueryExceptionConverter Singleton = new QueryExceptionConverter(); @@ -100,23 +145,25 @@ private QueryExceptionConverter() public override CosmosException Visit(MalformedContinuationTokenException malformedContinuationTokenException) { - return new BadRequestException( - $"{nameof(BadRequestException)} due to {nameof(MalformedContinuationTokenException)}", - malformedContinuationTokenException); + return CosmosExceptionFactory.CreateBadRequestException( + message: malformedContinuationTokenException.Message, + stackTrace: malformedContinuationTokenException.StackTrace, + innerException: malformedContinuationTokenException); } public override CosmosException Visit(UnexpectedQueryPartitionProviderException unexpectedQueryPartitionProviderException) { - return new InternalServerErrorException( - $"{nameof(InternalServerErrorException)} due to {nameof(UnexpectedQueryPartitionProviderException)}", - unexpectedQueryPartitionProviderException); + return CosmosExceptionFactory.CreateInternalServerErrorException( + message: $"{nameof(CosmosException)} due to {nameof(UnexpectedQueryPartitionProviderException)}", + innerException: unexpectedQueryPartitionProviderException); } public override CosmosException Visit(ExpectedQueryPartitionProviderException expectedQueryPartitionProviderException) { - return new BadRequestException( - $"{nameof(BadRequestException)} due to {nameof(ExpectedQueryPartitionProviderException)}", - expectedQueryPartitionProviderException); + return CosmosExceptionFactory.CreateBadRequestException( + message: expectedQueryPartitionProviderException.Message, + stackTrace: expectedQueryPartitionProviderException.StackTrace, + innerException: expectedQueryPartitionProviderException); } } } diff --git a/Microsoft.Azure.Cosmos/src/Query/v3Query/CosmosQueryClientCore.cs b/Microsoft.Azure.Cosmos/src/Query/v3Query/CosmosQueryClientCore.cs index 106c2cc472..24098c24ba 100644 --- a/Microsoft.Azure.Cosmos/src/Query/v3Query/CosmosQueryClientCore.cs +++ b/Microsoft.Azure.Cosmos/src/Query/v3Query/CosmosQueryClientCore.cs @@ -289,7 +289,7 @@ private QueryResponseCore GetCosmosElementResponse( return QueryResponseCore.CreateFailure( statusCode: cosmosResponseMessage.StatusCode, subStatusCodes: cosmosResponseMessage.Headers.SubStatusCode, - errorMessage: cosmosResponseMessage.ErrorMessage, + cosmosException: cosmosResponseMessage.CosmosException, requestCharge: cosmosResponseMessage.Headers.RequestCharge, activityId: cosmosResponseMessage.Headers.ActivityId, diagnostics: pageDiagnostics); diff --git a/Microsoft.Azure.Cosmos/src/Query/v3Query/QueryIterator.cs b/Microsoft.Azure.Cosmos/src/Query/v3Query/QueryIterator.cs index 3a6ab7b258..aa4cab2358 100644 --- a/Microsoft.Azure.Cosmos/src/Query/v3Query/QueryIterator.cs +++ b/Microsoft.Azure.Cosmos/src/Query/v3Query/QueryIterator.cs @@ -126,8 +126,7 @@ public override async Task ReadNextAsync(CancellationToken canc { queryResponse = QueryResponse.CreateFailure( statusCode: responseCore.StatusCode, - error: null, - errorMessage: responseCore.ErrorMessage, + cosmosException: responseCore.CosmosException, requestMessage: null, diagnostics: diagnostics, responseHeaders: new CosmosQueryResponseMessageHeaders( diff --git a/Microsoft.Azure.Cosmos/src/Query/v3Query/QueryResponse.cs b/Microsoft.Azure.Cosmos/src/Query/v3Query/QueryResponse.cs index fac7773717..5cbf7597cc 100644 --- a/Microsoft.Azure.Cosmos/src/Query/v3Query/QueryResponse.cs +++ b/Microsoft.Azure.Cosmos/src/Query/v3Query/QueryResponse.cs @@ -41,15 +41,13 @@ private QueryResponse( HttpStatusCode statusCode, RequestMessage requestMessage, CosmosDiagnosticsContext diagnostics, - string errorMessage, - Error error, + CosmosException cosmosException, Lazy memoryStream, CosmosSerializationFormatOptions serializationOptions) : base( statusCode: statusCode, requestMessage: requestMessage, - errorMessage: errorMessage, - error: error, + cosmosException: cosmosException, headers: responseHeaders, diagnostics: diagnostics) { @@ -120,8 +118,7 @@ internal static QueryResponse CreateSuccess( responseHeaders: responseHeaders, diagnostics: diagnostics, statusCode: HttpStatusCode.OK, - errorMessage: null, - error: null, + cosmosException: null, requestMessage: null, memoryStream: memoryStream, serializationOptions: serializationOptions); @@ -133,8 +130,7 @@ internal static QueryResponse CreateFailure( CosmosQueryResponseMessageHeaders responseHeaders, HttpStatusCode statusCode, RequestMessage requestMessage, - string errorMessage, - Error error, + CosmosException cosmosException, CosmosDiagnosticsContext diagnostics) { QueryResponse cosmosQueryResponse = new QueryResponse( @@ -144,8 +140,7 @@ internal static QueryResponse CreateFailure( responseHeaders: responseHeaders, diagnostics: diagnostics, statusCode: statusCode, - errorMessage: errorMessage, - error: error, + cosmosException: cosmosException, requestMessage: requestMessage, memoryStream: null, serializationOptions: null); diff --git a/Microsoft.Azure.Cosmos/src/Resource/ClientContextCore.cs b/Microsoft.Azure.Cosmos/src/Resource/ClientContextCore.cs index 1cf3a7437c..7ae2727837 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/ClientContextCore.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/ClientContextCore.cs @@ -11,6 +11,7 @@ namespace Microsoft.Azure.Cosmos using System.Threading.Tasks; using Microsoft.Azure.Cosmos.Handlers; using Microsoft.Azure.Cosmos.Query; + using Microsoft.Azure.Cosmos.Resource.CosmosExceptions; using Microsoft.Azure.Cosmos.Routing; using Microsoft.Azure.Documents; @@ -199,17 +200,21 @@ internal override async Task GetCachedContainerPropertiesAs string containerUri, CancellationToken cancellationToken = default(CancellationToken)) { + CosmosDiagnosticsContextCore diagnosticsContext = new CosmosDiagnosticsContextCore(); ClientCollectionCache collectionCache = await this.DocumentClient.GetCollectionCacheAsync(); try { - return await collectionCache.ResolveByNameAsync( - HttpConstants.Versions.CurrentVersion, - containerUri, - cancellationToken); + using (diagnosticsContext.CreateScope("ContainerCache.ResolveByNameAsync")) + { + return await collectionCache.ResolveByNameAsync( + HttpConstants.Versions.CurrentVersion, + containerUri, + cancellationToken); + } } catch (DocumentClientException ex) { - throw new CosmosException(ex.ToCosmosResponseMessage(null), ex.Message, ex.Error); + throw CosmosExceptionFactory.Create(ex, diagnosticsContext); } } diff --git a/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerCore.cs b/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerCore.cs index 15fe117605..b64ebd395a 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerCore.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/Container/ContainerCore.cs @@ -11,6 +11,7 @@ namespace Microsoft.Azure.Cosmos using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Cosmos.Query.Core.QueryClient; + using Microsoft.Azure.Cosmos.Resource.CosmosExceptions; using Microsoft.Azure.Cosmos.Routing; using Microsoft.Azure.Cosmos.Scripts; using Microsoft.Azure.Documents; @@ -379,7 +380,9 @@ async Task> GetPartitionKeyRangesAsync( } catch (DocumentClientException ex) { - throw new CosmosException(ex.ToCosmosResponseMessage(null), ex.Message, ex.Error); + throw CosmosExceptionFactory.Create( + dce: ex, + diagnosticsContext: null); } } diff --git a/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/BadRequestException.cs b/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/BadRequestException.cs deleted file mode 100644 index c7646d369b..0000000000 --- a/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/BadRequestException.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -// ------------------------------------------------------------ - -namespace Microsoft.Azure.Cosmos.Resource.CosmosExceptions -{ - using System; - using System.Net; - - internal sealed class BadRequestException : CosmosHttpException - { - public BadRequestException() - : base(statusCode: HttpStatusCode.BadRequest, message: null) - { - } - - public BadRequestException(string message) - : base(statusCode: HttpStatusCode.BadRequest, message: message) - { - } - - public BadRequestException(string message, Exception innerException) - : base(statusCode: HttpStatusCode.BadRequest, message: message, innerException: innerException) - { - } - } -} diff --git a/Microsoft.Azure.Cosmos/src/Resource/CosmosException.cs b/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosException.cs similarity index 66% rename from Microsoft.Azure.Cosmos/src/Resource/CosmosException.cs rename to Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosException.cs index 0f95bc120d..0d797f9848 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/CosmosException.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosException.cs @@ -5,7 +5,6 @@ namespace Microsoft.Azure.Cosmos { using System; - using System.IO; using System.Net; using System.Text; using Microsoft.Azure.Documents; @@ -15,49 +14,33 @@ namespace Microsoft.Azure.Cosmos /// public class CosmosException : Exception { - private static readonly string FullName = typeof(CosmosException).FullName; - internal CosmosException( - HttpStatusCode statusCode, - string message, - Error error = null, - Exception inner = null) - : base(message, inner) - { - this.StatusCode = statusCode; - this.Error = error; - this.Headers = new Headers(); - } + private readonly string stackTrace; internal CosmosException( - ResponseMessage cosmosResponseMessage, + HttpStatusCode statusCodes, string message, - Error error = null) - : base(message) + int subStatusCode, + string stackTrace, + string activityId, + double requestCharge, + TimeSpan? retryAfter, + Headers headers, + CosmosDiagnosticsContext diagnosticsContext, + Error error, + Exception innerException) + : base(MergeErrorMessages(message, error), innerException) { - if (cosmosResponseMessage != null) - { - this.StatusCode = cosmosResponseMessage.StatusCode; - this.Headers = cosmosResponseMessage.Headers; - if (this.Headers == null) - { - this.Headers = new Headers(); - } - - this.ActivityId = this.Headers.ActivityId; - this.RequestCharge = this.Headers.RequestCharge; - this.RetryAfter = this.Headers.RetryAfter; - this.SubStatusCode = (int)this.Headers.SubStatusCode; - this.Diagnostics = cosmosResponseMessage.Diagnostics; - if (this.Headers.ContentLengthAsLong > 0) - { - using (StreamReader responseReader = new StreamReader(cosmosResponseMessage.Content)) - { - this.ResponseBody = responseReader.ReadToEnd(); - } - } - } - + this.stackTrace = stackTrace; + this.ActivityId = activityId; + this.StatusCode = statusCodes; + this.SubStatusCode = subStatusCode; + this.RetryAfter = retryAfter; + this.RequestCharge = requestCharge; + this.Headers = headers; this.Error = error; + + // Always have a diagnostic context. A new diagnostic will have useful info like user agent + this.DiagnosticsContext = diagnosticsContext ?? CosmosDiagnosticsContext.Create(); } /// @@ -76,11 +59,13 @@ public CosmosException( double requestCharge) : base(message) { + this.stackTrace = null; this.SubStatusCode = subStatusCode; this.StatusCode = statusCode; this.RequestCharge = requestCharge; this.ActivityId = activityId; this.Headers = new Headers(); + this.DiagnosticsContext = CosmosDiagnosticsContext.Create(); } /// @@ -129,12 +114,30 @@ public CosmosException( /// /// Gets the diagnostics for the request /// - public virtual CosmosDiagnostics Diagnostics { get; } + public virtual CosmosDiagnostics Diagnostics => this.DiagnosticsContext; + + /// + public override string StackTrace + { + get + { + if (this.stackTrace != null) + { + return this.stackTrace; + } + else + { + return base.StackTrace; + } + } + } + + internal virtual CosmosDiagnosticsContext DiagnosticsContext { get; } /// - /// Gets the internal error object + /// Gets the internal error object. /// - internal virtual Error Error { get; } + internal virtual Documents.Error Error { get; set; } /// /// Try to get a header from the cosmos response message @@ -158,9 +161,47 @@ public virtual bool TryGetHeader(string headerName, out string value) /// /// A string representation of the exception. public override string ToString() + { + return this.ToStringHelper(true); + } + + internal string ToString(bool includeDiagnostics) + { + return this.ToStringHelper(includeDiagnostics); + } + + internal ResponseMessage ToCosmosResponseMessage(RequestMessage request) + { + return new ResponseMessage( + headers: this.Headers, + requestMessage: request, + cosmosException: this, + statusCode: this.StatusCode, + diagnostics: this.DiagnosticsContext); + } + + /// + /// This handles the scenario there is a message and the error object is set. + /// + private static string MergeErrorMessages(string message, Error error) + { + if (error == null) + { + return message; + } + + if (string.IsNullOrEmpty(message)) + { + return error.Message; + } + + return $"{message}; Inner Message:{error.Message}"; + } + + private string ToStringHelper(bool includeDiagnostics) { StringBuilder stringBuilder = new StringBuilder(); - stringBuilder.Append(CosmosException.FullName); + stringBuilder.Append(this.GetType().FullName); if (this.Message != null) { stringBuilder.Append(" : "); @@ -168,6 +209,14 @@ public override string ToString() stringBuilder.AppendLine(); } + if (this.Error != null) + { + stringBuilder.Append(" : "); + stringBuilder.Append($"Code :{this.Error.Code ?? string.Empty}; Details :{this.Error.ErrorDetails ?? string.Empty}; "); + stringBuilder.Append($"Additional Details: {this.Error.AdditionalErrorInfo ?? string.Empty};"); + stringBuilder.AppendLine(); + } + stringBuilder.AppendFormat("StatusCode = {0};", this.StatusCode); stringBuilder.AppendLine(); @@ -180,7 +229,7 @@ public override string ToString() stringBuilder.AppendFormat("RequestCharge = {0};", this.RequestCharge); stringBuilder.AppendLine(); - if (this.Diagnostics != null) + if (includeDiagnostics && this.Diagnostics != null) { stringBuilder.Append(this.Diagnostics); stringBuilder.AppendLine(); @@ -200,16 +249,5 @@ public override string ToString() return stringBuilder.ToString(); } - - internal ResponseMessage ToCosmosResponseMessage(RequestMessage request) - { - return new ResponseMessage( - headers: this.Headers, - requestMessage: request, - errorMessage: this.Message, - statusCode: this.StatusCode, - error: this.Error, - diagnostics: request.DiagnosticsContext); - } } } diff --git a/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosExceptionFactory.cs b/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosExceptionFactory.cs new file mode 100644 index 0000000000..d83ae36ae0 --- /dev/null +++ b/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosExceptionFactory.cs @@ -0,0 +1,372 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ + +namespace Microsoft.Azure.Cosmos.Resource.CosmosExceptions +{ + using System; + using System.IO; + using System.Net; + using Microsoft.Azure.Documents; + + internal static class CosmosExceptionFactory + { + internal static CosmosException Create( + DocumentClientException dce, + CosmosDiagnosticsContext diagnosticsContext) + { + Headers headers = new Headers(); + if (dce.Headers != null) + { + foreach (string key in dce.Headers) + { + headers.Add(key, dce.Headers[key]); + } + } + + HttpStatusCode httpStatusCode; + if (dce.StatusCode.HasValue) + { + httpStatusCode = dce.StatusCode.Value; + } + else if (dce.InnerException != null && dce.InnerException is TransportException) + { + httpStatusCode = HttpStatusCode.RequestTimeout; + } + else + { + httpStatusCode = HttpStatusCode.InternalServerError; + } + + return CosmosExceptionFactory.Create( + httpStatusCode, + (int)dce.GetSubStatus(), + dce.Message, + dce.StackTrace, + dce.ActivityId, + dce.RequestCharge, + dce.RetryAfter, + headers, + diagnosticsContext, + dce.Error, + dce.InnerException); + } + + internal static CosmosException Create( + HttpStatusCode statusCode, + RequestMessage requestMessage, + string errorMessage) + { + return CosmosExceptionFactory.Create( + statusCode: statusCode, + subStatusCode: default, + message: errorMessage, + stackTrace: null, + activityId: requestMessage?.Headers?.ActivityId, + requestCharge: 0, + retryAfter: default, + headers: requestMessage?.Headers, + diagnosticsContext: default, + error: default, + innerException: default); + } + + internal static CosmosException Create( + ResponseMessage responseMessage) + { + // If there is no content and there is cosmos exception + // then use the existing exception + if (responseMessage.Content == null + && responseMessage.CosmosException != null) + { + return responseMessage.CosmosException; + } + + // If content was added after the response message + // creation the exception should be updated. + string errorMessage = responseMessage.CosmosException?.Message; + (Error error, string contentMessage) = CosmosExceptionFactory.GetErrorFromStream(responseMessage.Content); + if (!string.IsNullOrEmpty(contentMessage)) + { + if (string.IsNullOrEmpty(errorMessage)) + { + errorMessage = contentMessage; + } + else + { + errorMessage = $"Error Message: {errorMessage}; Content {contentMessage};"; + } + } + + return CosmosExceptionFactory.Create( + responseMessage.StatusCode, + (int)responseMessage.Headers.SubStatusCode, + errorMessage, + responseMessage?.CosmosException?.StackTrace, + responseMessage.Headers.ActivityId, + responseMessage.Headers.RequestCharge, + responseMessage.Headers.RetryAfter, + responseMessage.Headers, + responseMessage.DiagnosticsContext, + error, + responseMessage.CosmosException?.InnerException); + } + + internal static CosmosException Create( + DocumentServiceResponse documentServiceResponse, + Headers responseHeaders, + RequestMessage requestMessage) + { + if (documentServiceResponse == null) + { + throw new ArgumentNullException(nameof(documentServiceResponse)); + } + + if (requestMessage == null) + { + throw new ArgumentNullException(nameof(requestMessage)); + } + + if (responseHeaders == null) + { + responseHeaders = documentServiceResponse.Headers.ToCosmosHeaders(); + } + + (Error error, string errorMessage) = CosmosExceptionFactory.GetErrorFromStream(documentServiceResponse.ResponseBody); + + return CosmosExceptionFactory.Create( + statusCode: documentServiceResponse.StatusCode, + subStatusCode: (int)responseHeaders.SubStatusCode, + message: errorMessage, + stackTrace: null, + activityId: responseHeaders.ActivityId, + requestCharge: responseHeaders.RequestCharge, + retryAfter: responseHeaders.RetryAfter, + headers: responseHeaders, + diagnosticsContext: requestMessage.DiagnosticsContext, + error: error, + innerException: null); + } + + internal static CosmosException Create( + StoreResponse storeResponse, + RequestMessage requestMessage) + { + if (storeResponse == null) + { + throw new ArgumentNullException(nameof(storeResponse)); + } + + if (requestMessage == null) + { + throw new ArgumentNullException(nameof(requestMessage)); + } + + (Error error, string errorMessage) = CosmosExceptionFactory.GetErrorFromStream(storeResponse.ResponseBody); + Headers headers = storeResponse.ToCosmosHeaders(); + + return CosmosExceptionFactory.Create( + statusCode: storeResponse.StatusCode, + subStatusCode: (int)headers.SubStatusCode, + message: errorMessage, + stackTrace: null, + activityId: headers.ActivityId, + requestCharge: headers.RequestCharge, + retryAfter: headers.RetryAfter, + headers: headers, + diagnosticsContext: requestMessage.DiagnosticsContext, + error: error, + innerException: null); + } + + internal static (Error, string) GetErrorFromStream( + Stream content) + { + using (content) + { + if (content != null + && content.CanRead) + { + try + { + Error error = Documents.Resource.LoadFrom(content); + if (error != null) + { + // Error format is not consistent across modes + return (error, null); + } + } + catch (Newtonsoft.Json.JsonReaderException) + { + } + + // Content is not Json + content.Position = 0; + using (StreamReader streamReader = new StreamReader(content)) + { + return (null, streamReader.ReadToEnd()); + } + } + + return (null, null); + } + } + + internal static CosmosException CreateRequestTimeoutException( + string message, + int subStatusCode = default, + string stackTrace = default, + string activityId = default, + double requestCharge = default, + TimeSpan? retryAfter = default, + Headers headers = default, + CosmosDiagnosticsContext diagnosticsContext = default, + Error error = default, + Exception innerException = default) + { + return CosmosExceptionFactory.Create( + HttpStatusCode.RequestTimeout, + subStatusCode, + message, + stackTrace, + activityId, + requestCharge, + retryAfter, + headers, + diagnosticsContext, + error, + innerException); + } + + internal static CosmosException CreateThrottledException( + string message, + int subStatusCode = default, + string stackTrace = default, + string activityId = default, + double requestCharge = default, + TimeSpan? retryAfter = default, + Headers headers = default, + CosmosDiagnosticsContext diagnosticsContext = default, + Error error = default, + Exception innerException = default) + { + return CosmosExceptionFactory.Create( + (HttpStatusCode)StatusCodes.TooManyRequests, + subStatusCode, + message, + stackTrace, + activityId, + requestCharge, + retryAfter, + headers, + diagnosticsContext, + error, + innerException); + } + + internal static CosmosException CreateNotFoundException( + string message, + int subStatusCode = default, + string stackTrace = default, + string activityId = default, + double requestCharge = default, + TimeSpan? retryAfter = default, + Headers headers = default, + CosmosDiagnosticsContext diagnosticsContext = default, + Error error = default, + Exception innerException = default) + { + return CosmosExceptionFactory.Create( + HttpStatusCode.NotFound, + subStatusCode, + message, + stackTrace, + activityId, + requestCharge, + retryAfter, + headers, + diagnosticsContext, + error, + innerException); + } + + internal static CosmosException CreateInternalServerErrorException( + string message, + int subStatusCode = default, + string stackTrace = default, + string activityId = default, + double requestCharge = default, + TimeSpan? retryAfter = default, + Headers headers = default, + CosmosDiagnosticsContext diagnosticsContext = default, + Error error = default, + Exception innerException = default) + { + return CosmosExceptionFactory.Create( + HttpStatusCode.InternalServerError, + subStatusCode, + message, + stackTrace, + activityId, + requestCharge, + retryAfter, + headers, + diagnosticsContext, + error, + innerException); + } + + internal static CosmosException CreateBadRequestException( + string message, + int subStatusCode = default, + string stackTrace = default, + string activityId = default, + double requestCharge = default, + TimeSpan? retryAfter = default, + Headers headers = default, + CosmosDiagnosticsContext diagnosticsContext = default, + Error error = default, + Exception innerException = default) + { + return CosmosExceptionFactory.Create( + HttpStatusCode.BadRequest, + subStatusCode, + message, + stackTrace, + activityId, + requestCharge, + retryAfter, + headers, + diagnosticsContext, + error, + innerException); + } + + internal static CosmosException Create( + HttpStatusCode statusCode, + int subStatusCode, + string message, + string stackTrace, + string activityId, + double requestCharge, + TimeSpan? retryAfter, + Headers headers, + CosmosDiagnosticsContext diagnosticsContext, + Error error, + Exception innerException) + { + return new CosmosException( + statusCode, + message, + subStatusCode, + stackTrace, + activityId, + requestCharge, + retryAfter, + headers, + diagnosticsContext, + error, + innerException); + } + } +} diff --git a/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosHttpException.cs b/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosHttpException.cs deleted file mode 100644 index 1b1a76b486..0000000000 --- a/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/CosmosHttpException.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -// ------------------------------------------------------------ - -namespace Microsoft.Azure.Cosmos.Resource.CosmosExceptions -{ - using System; - using System.Net; - - internal abstract class CosmosHttpException : CosmosException - { - protected CosmosHttpException(HttpStatusCode statusCode) - : this(statusCode, message: null, innerException: null) - { - } - - protected CosmosHttpException(HttpStatusCode statusCode, string message) - : this(statusCode, message: message, innerException: null) - { - } - - protected CosmosHttpException(HttpStatusCode statusCode, string message, Exception innerException) - : base(statusCode: statusCode, message: message, inner: innerException) - { - } - } -} diff --git a/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/InternalServerErrorException.cs b/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/InternalServerErrorException.cs deleted file mode 100644 index 4d4ceb9122..0000000000 --- a/Microsoft.Azure.Cosmos/src/Resource/CosmosExceptions/InternalServerErrorException.cs +++ /dev/null @@ -1,27 +0,0 @@ -// ------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -// ------------------------------------------------------------ - -namespace Microsoft.Azure.Cosmos.Resource.CosmosExceptions -{ - using System; - using System.Net; - - internal sealed class InternalServerErrorException : CosmosHttpException - { - public InternalServerErrorException() - : base(statusCode: HttpStatusCode.InternalServerError, message: null) - { - } - - public InternalServerErrorException(string message) - : base(statusCode: HttpStatusCode.InternalServerError, message: message) - { - } - - public InternalServerErrorException(string message, Exception innerException) - : base(statusCode: HttpStatusCode.InternalServerError, message: message, innerException: innerException) - { - } - } -} diff --git a/Microsoft.Azure.Cosmos/src/Resource/DataEncryptionKey/DataEncryptionKeyCore.cs b/Microsoft.Azure.Cosmos/src/Resource/DataEncryptionKey/DataEncryptionKeyCore.cs index dfc71ad6c1..5ffd3a221a 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/DataEncryptionKey/DataEncryptionKeyCore.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/DataEncryptionKey/DataEncryptionKeyCore.cs @@ -11,6 +11,7 @@ namespace Microsoft.Azure.Cosmos using System.Net; using System.Threading; using System.Threading.Tasks; + using Microsoft.Azure.Cosmos.Resource.CosmosExceptions; using Microsoft.Azure.Documents; /// @@ -135,7 +136,10 @@ internal static Uri CreateLinkUri(CosmosClientContext clientContext, DatabaseCor } catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { - throw new CosmosException(HttpStatusCode.NotFound, ClientResources.DataEncryptionKeyNotFound, inner: ex); + throw CosmosExceptionFactory.CreateNotFoundException( + ClientResources.DataEncryptionKeyNotFound, + diagnosticsContext: diagnosticsContext, + innerException: ex); } InMemoryRawDek inMemoryRawDek = await this.ClientContext.DekCache.GetOrAddRawDekAsync( @@ -172,7 +176,10 @@ internal static Uri CreateLinkUri(CosmosClientContext clientContext, DatabaseCor } catch (CosmosException ex) when (ex.StatusCode == HttpStatusCode.NotFound) { - throw new CosmosException(HttpStatusCode.NotFound, ClientResources.DataEncryptionKeyNotFound, inner: ex); + throw CosmosExceptionFactory.CreateNotFoundException( + ClientResources.DataEncryptionKeyNotFound, + diagnosticsContext: diagnosticsContext, + innerException: ex); } InMemoryRawDek inMemoryRawDek = await this.ClientContext.DekCache.GetOrAddRawDekAsync( @@ -223,7 +230,8 @@ internal virtual byte[] GenerateKey(CosmosEncryptionAlgorithm encryptionAlgorith InMemoryRawDek roundTripResponse = await this.UnwrapAsync(tempDekProperties, diagnosticsContext, cancellationToken); if (!roundTripResponse.RawDek.SequenceEqual(key)) { - throw new CosmosException(HttpStatusCode.BadRequest, ClientResources.KeyWrappingDidNotRoundtrip); + throw CosmosExceptionFactory.CreateBadRequestException(ClientResources.KeyWrappingDidNotRoundtrip, + diagnosticsContext: diagnosticsContext); } return (keyWrapResponse.WrappedDataEncryptionKey, keyWrapResponse.EncryptionKeyWrapMetadata, roundTripResponse); diff --git a/Microsoft.Azure.Cosmos/src/Resource/Offer/CosmosOffers.cs b/Microsoft.Azure.Cosmos/src/Resource/Offer/CosmosOffers.cs index 34bc33629a..f5750f9bd8 100644 --- a/Microsoft.Azure.Cosmos/src/Resource/Offer/CosmosOffers.cs +++ b/Microsoft.Azure.Cosmos/src/Resource/Offer/CosmosOffers.cs @@ -11,6 +11,7 @@ namespace Microsoft.Azure.Cosmos using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Cosmos.Handlers; + using Microsoft.Azure.Cosmos.Resource.CosmosExceptions; using Microsoft.Azure.Documents; internal class CosmosOffers @@ -145,7 +146,8 @@ private async Task GetOfferV2Async( if (offerV2 == null && failIfNotConfigured) { - throw new CosmosException(HttpStatusCode.NotFound, $"Throughput is not configured for {targetRID}"); + throw (CosmosException)CosmosExceptionFactory.CreateNotFoundException( + $"Throughput is not configured for {targetRID}"); } return offerV2; diff --git a/Microsoft.Azure.Cosmos/src/Util/Extensions.cs b/Microsoft.Azure.Cosmos/src/Util/Extensions.cs index 6045ae0e90..819148626b 100644 --- a/Microsoft.Azure.Cosmos/src/Util/Extensions.cs +++ b/Microsoft.Azure.Cosmos/src/Util/Extensions.cs @@ -13,93 +13,96 @@ namespace Microsoft.Azure.Cosmos using System.Threading.Tasks; using Microsoft.Azure.Cosmos.Core.Trace; using Microsoft.Azure.Cosmos.Diagnostics; + using Microsoft.Azure.Cosmos.Resource.CosmosExceptions; using Microsoft.Azure.Documents; + using Microsoft.Azure.Documents.Collections; internal static class Extensions { private static readonly char[] NewLineCharacters = new[] { '\r', '\n' }; + internal static bool IsSuccess(this HttpStatusCode httpStatusCode) + { + return ((int)httpStatusCode >= 200) && ((int)httpStatusCode <= 299); + } + internal static ResponseMessage ToCosmosResponseMessage(this DocumentServiceResponse documentServiceResponse, RequestMessage requestMessage) { Debug.Assert(requestMessage != null, nameof(requestMessage)); - ResponseMessage responseMessage = new ResponseMessage(documentServiceResponse.StatusCode, requestMessage); - if (documentServiceResponse.ResponseBody != null) - { - responseMessage.Content = documentServiceResponse.ResponseBody; - } - - if (documentServiceResponse.Headers != null) - { - foreach (string key in documentServiceResponse.Headers) - { - responseMessage.Headers.Add(key, documentServiceResponse.Headers[key]); - } - } - + Headers headers = documentServiceResponse.Headers.ToCosmosHeaders(); CosmosClientSideRequestStatistics cosmosClientSideRequestStatistics = documentServiceResponse.RequestStats as CosmosClientSideRequestStatistics; - PointOperationStatistics pointOperationStatistics = new PointOperationStatistics( - activityId: responseMessage.Headers.ActivityId, + requestMessage.DiagnosticsContext.AddDiagnosticsInternal(new PointOperationStatistics( + activityId: headers.ActivityId, statusCode: documentServiceResponse.StatusCode, subStatusCode: documentServiceResponse.SubStatusCode, - requestCharge: responseMessage.Headers.RequestCharge, - errorMessage: responseMessage.ErrorMessage, + requestCharge: headers.RequestCharge, + errorMessage: null, method: requestMessage?.Method, requestUri: requestMessage?.RequestUri, requestSessionToken: requestMessage?.Headers?.Session, - responseSessionToken: responseMessage.Headers.Session, - clientSideRequestStatistics: cosmosClientSideRequestStatistics); + responseSessionToken: headers.Session, + clientSideRequestStatistics: cosmosClientSideRequestStatistics)); - requestMessage.DiagnosticsContext.AddDiagnosticsInternal(pointOperationStatistics); - return responseMessage; - } - - internal static ResponseMessage ToCosmosResponseMessage(this DocumentClientException documentClientException, RequestMessage requestMessage) - { - // if StatusCode is null it is a client business logic error and it never hit the backend, so throw - if (documentClientException.StatusCode == null) + // If it's considered a failure create the corresponding CosmosException + if (!documentServiceResponse.StatusCode.IsSuccess()) { - throw documentClientException; + CosmosException cosmosException = CosmosExceptionFactory.Create( + documentServiceResponse, + headers, + requestMessage); + + return cosmosException.ToCosmosResponseMessage(requestMessage); } - // if there is a status code then it came from the backend, return error as http error instead of throwing the exception - ResponseMessage responseMessage = new ResponseMessage(documentClientException.StatusCode ?? HttpStatusCode.InternalServerError, requestMessage); - string reasonPhraseString = documentClientException.ToString(); - if (!string.IsNullOrEmpty(reasonPhraseString)) + ResponseMessage responseMessage = new ResponseMessage( + statusCode: documentServiceResponse.StatusCode, + requestMessage: requestMessage, + headers: headers, + cosmosException: null, + diagnostics: requestMessage.DiagnosticsContext) { - if (documentClientException.Message.IndexOfAny(Extensions.NewLineCharacters) >= 0) - { - StringBuilder sb = new StringBuilder(reasonPhraseString); - sb = sb.Replace("\r", string.Empty); - sb = sb.Replace("\n", string.Empty); - reasonPhraseString = sb.ToString(); - } - } + Content = documentServiceResponse.ResponseBody + }; - responseMessage.ErrorMessage = reasonPhraseString; - responseMessage.Error = documentClientException.Error; + return responseMessage; + } - if (documentClientException.Headers != null) + internal static ResponseMessage ToCosmosResponseMessage(this DocumentClientException documentClientException, RequestMessage requestMessage) + { + CosmosDiagnosticsContext diagnosticsContext = requestMessage?.DiagnosticsContext; + if (diagnosticsContext == null) { - foreach (string header in documentClientException.Headers.AllKeys()) - { - responseMessage.Headers.Add(header, documentClientException.Headers[header]); - } + diagnosticsContext = CosmosDiagnosticsContextCore.Create(); } + CosmosException cosmosException = CosmosExceptionFactory.Create( + documentClientException, + diagnosticsContext); + PointOperationStatistics pointOperationStatistics = new PointOperationStatistics( - activityId: responseMessage.Headers.ActivityId, - statusCode: documentClientException.StatusCode.Value, + activityId: cosmosException.Headers.ActivityId, + statusCode: cosmosException.StatusCode, subStatusCode: (int)SubStatusCodes.Unknown, - requestCharge: responseMessage.Headers.RequestCharge, - errorMessage: responseMessage.ErrorMessage, + requestCharge: cosmosException.Headers.RequestCharge, + errorMessage: cosmosException.Message, method: requestMessage?.Method, requestUri: requestMessage?.RequestUri, requestSessionToken: requestMessage?.Headers?.Session, - responseSessionToken: responseMessage.Headers.Session, + responseSessionToken: cosmosException.Headers.Session, clientSideRequestStatistics: documentClientException.RequestStatistics as CosmosClientSideRequestStatistics); - responseMessage.DiagnosticsContext.AddDiagnosticsInternal(pointOperationStatistics); + diagnosticsContext.AddDiagnosticsInternal(pointOperationStatistics); + + // if StatusCode is null it is a client business logic error and it never hit the backend, so throw + if (documentClientException.StatusCode == null) + { + throw cosmosException; + } + + // if there is a status code then it came from the backend, return error as http error instead of throwing the exception + ResponseMessage responseMessage = cosmosException.ToCosmosResponseMessage(requestMessage); + if (requestMessage != null) { requestMessage.Properties.Remove(nameof(DocumentClientException)); @@ -111,6 +114,16 @@ internal static ResponseMessage ToCosmosResponseMessage(this DocumentClientExcep internal static ResponseMessage ToCosmosResponseMessage(this StoreResponse storeResponse, RequestMessage requestMessage) { + // If it's considered a failure create the corresponding CosmosException + if (!storeResponse.StatusCode.IsSuccess()) + { + CosmosException cosmosException = CosmosExceptionFactory.Create( + storeResponse, + requestMessage); + + return cosmosException.ToCosmosResponseMessage(requestMessage); + } + // Is status code conversion lossy? ResponseMessage responseMessage = new ResponseMessage((HttpStatusCode)storeResponse.Status, requestMessage); if (storeResponse.ResponseBody != null) @@ -118,12 +131,29 @@ internal static ResponseMessage ToCosmosResponseMessage(this StoreResponse store responseMessage.Content = storeResponse.ResponseBody; } + return responseMessage; + } + + internal static Headers ToCosmosHeaders(this StoreResponse storeResponse) + { + Headers headers = new Headers(); for (int i = 0; i < storeResponse.ResponseHeaderNames.Length; i++) { - responseMessage.Headers.Add(storeResponse.ResponseHeaderNames[i], storeResponse.ResponseHeaderValues[i]); + headers.Add(storeResponse.ResponseHeaderNames[i], storeResponse.ResponseHeaderValues[i]); } - return responseMessage; + return headers; + } + + internal static Headers ToCosmosHeaders(this INameValueCollection nameValueCollection) + { + Headers headers = new Headers(); + foreach (string key in nameValueCollection) + { + headers.Add(key, nameValueCollection[key]); + } + + return headers; } internal static void TraceException(Exception exception) diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/BaselineTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/BaselineTests.cs index c41c2de489..e94c04fca7 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/BaselineTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/BaselineTests.cs @@ -139,7 +139,9 @@ public void ExecuteTestSuite(IEnumerable inputs, [CallerMemberName] stri matched, $@" Expected: {baselineTextSuffix}, - Actual: {outputTextSuffix}"); + Actual: {outputTextSuffix}, + OutputPath: {outputPath}, + BaselinePath: {baselinePath}"); } /// diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqGeneralBaselineTests.TestThenByTranslation.xml b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqGeneralBaselineTests.TestThenByTranslation.xml index 1705c5db5b..7b5f11cb11 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqGeneralBaselineTests.TestThenByTranslation.xml +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/BaselineTest/TestBaseline/LinqGeneralBaselineTests.TestThenByTranslation.xml @@ -415,7 +415,7 @@ FROM ( ) AS r1 ORDER BY r1["FamilyId"] ASC, r1["FamilyNumber"] ASC ]]> - + @@ -467,7 +467,7 @@ FROM ( ) AS r0 ORDER BY r0["FamilyId"] ASC, r0["FamilyNumber"] ASC ]]> - + diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/ChangeFeed/SmokeTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/ChangeFeed/SmokeTests.cs index b15023554a..92801fe474 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/ChangeFeed/SmokeTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/ChangeFeed/SmokeTests.cs @@ -11,6 +11,7 @@ namespace Microsoft.Azure.Cosmos.SDK.EmulatorTests.ChangeFeed using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Cosmos.Query.Core; + using Microsoft.Azure.Cosmos.Resource.CosmosExceptions; using Microsoft.VisualStudio.TestTools.UnitTesting; [TestClass] diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosContainerTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosContainerTests.cs index 1dbdb6dbef..4b32bba569 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosContainerTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosContainerTests.cs @@ -10,6 +10,7 @@ namespace Microsoft.Azure.Cosmos.SDK.EmulatorTests using System.Linq; using System.Net; using System.Threading.Tasks; + using Microsoft.Azure.Cosmos.Resource.CosmosExceptions; using Microsoft.Azure.Documents; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json.Linq; diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosDiagnosticsTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosDiagnosticsTests.cs index f92f115025..450d1b4f5e 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosDiagnosticsTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CosmosDiagnosticsTests.cs @@ -6,6 +6,7 @@ namespace Microsoft.Azure.Cosmos.SDK.EmulatorTests { using Microsoft.Azure.Cosmos.Query.Core; using Microsoft.VisualStudio.TestTools.UnitTesting; + using Moq; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; @@ -71,6 +72,53 @@ public async Task CustomHandlersDiagnostic() await databaseResponse.Database.DeleteAsync(); } + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public async Task PointOperationRequestTimeoutDiagnostic(bool disableDiagnostics) + { + ItemRequestOptions requestOptions = new ItemRequestOptions() + { + DiagnosticContext = disableDiagnostics ? EmptyCosmosDiagnosticsContext.Singleton : null + }; + + Guid exceptionActivityId = Guid.NewGuid(); + string transportExceptionDescription = "transportExceptionDescription" + Guid.NewGuid(); + Container containerWithTransportException = TransportClientHelper.GetContainerWithItemTransportException( + this.database.Id, + this.Container.Id, + exceptionActivityId, + transportExceptionDescription); + + //Checking point operation diagnostics on typed operations + ToDoActivity testItem = ToDoActivity.CreateRandomToDoActivity(); + try + { + ItemResponse createResponse = await containerWithTransportException.CreateItemAsync( + item: testItem, + requestOptions: requestOptions); + Assert.Fail("Should have thrown a request timeout exception"); + } + catch(CosmosException ce) when (ce.StatusCode == System.Net.HttpStatusCode.RequestTimeout) + { + string exception = ce.ToString(); + Assert.IsNotNull(exception); + Assert.IsTrue(exception.Contains(exceptionActivityId.ToString())); + Assert.IsTrue(exception.Contains(transportExceptionDescription)); + + string diagnosics = ce.Diagnostics.ToString(); + if (disableDiagnostics) + { + Assert.IsTrue(string.IsNullOrEmpty(diagnosics)); + } + else + { + Assert.IsFalse(string.IsNullOrEmpty(diagnosics)); + Assert.IsTrue(exception.Contains(diagnosics)); + } + } + } + [TestMethod] [DataRow(true)] [DataRow(false)] @@ -92,6 +140,7 @@ public async Task PointOperationDiagnostic(bool disableDiagnostics) id: testItem.id, partitionKey: new PartitionKey(testItem.status), requestOptions); + CosmosDiagnosticsTests.VerifyPointDiagnostics(readResponse.Diagnostics, disableDiagnostics); Assert.IsNotNull(readResponse.Diagnostics); testItem.description = "NewDescription"; @@ -102,7 +151,7 @@ public async Task PointOperationDiagnostic(bool disableDiagnostics) requestOptions: requestOptions); Assert.AreEqual(replaceResponse.Resource.description, "NewDescription"); - CosmosDiagnosticsTests.VerifyPointDiagnostics(createResponse.Diagnostics, disableDiagnostics); + CosmosDiagnosticsTests.VerifyPointDiagnostics(replaceResponse.Diagnostics, disableDiagnostics); ItemResponse deleteResponse = await this.Container.DeleteItemAsync( partitionKey: new Cosmos.PartitionKey(testItem.status), diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CrossPartitionQueryTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CrossPartitionQueryTests.cs index b2b1989397..a2773f5b76 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CrossPartitionQueryTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/CrossPartitionQueryTests.cs @@ -27,6 +27,7 @@ namespace Microsoft.Azure.Cosmos.SDK.EmulatorTests using Microsoft.Azure.Cosmos.Query.Core.Metrics; using Microsoft.Azure.Cosmos.Query.Core.QueryClient; using Microsoft.Azure.Cosmos.Query.Core.QueryPlan; + using Microsoft.Azure.Cosmos.Resource.CosmosExceptions; using Microsoft.Azure.Cosmos.Routing; using Microsoft.Azure.Documents; using Microsoft.Azure.Documents.Routing; @@ -796,7 +797,7 @@ private async Task TestBadQueriesOverMultiplePartitionsHelper(Container containe } catch (CosmosException exception) when (exception.StatusCode == HttpStatusCode.BadRequest) { - Assert.IsTrue(exception.Message.Contains(@"{""errors"":[{""severity"":""Error"",""location"":{""start"":27,""end"":28},""code"":""SC2001"",""message"":""Identifier 'a' could not be resolved.""}]}"), + Assert.IsTrue(exception.Message.Contains(@"Identifier 'a' could not be resolved."), exception.Message); } } diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Utils/TransportClientHelper.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Utils/TransportClientHelper.cs new file mode 100644 index 0000000000..a0bb0ba87b --- /dev/null +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.EmulatorTests/Utils/TransportClientHelper.cs @@ -0,0 +1,91 @@ +//------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +//------------------------------------------------------------ + +namespace Microsoft.Azure.Cosmos.SDK.EmulatorTests +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Text; + using System.Threading.Tasks; + using Microsoft.Azure.Cosmos.SDK.EmulatorTests; + using Microsoft.Azure.Documents; + + + internal static class TransportClientHelper + { + internal static Container GetContainerWithItemTransportException( + string databaseId, + string containerId, + Guid activityId, + string transportExceptionSourceDescription) + { + CosmosClient clientWithIntercepter = TestCommon.CreateCosmosClient( + builder => + { + builder.WithTransportClientHandlerFactory(transportClient => new TransportClientWrapper( + transportClient, + (uri, resourceOperation, request) => TransportClientHelper.ThrowTransportExceptionOnItemOperation( + uri, + resourceOperation, + request, + activityId, + transportExceptionSourceDescription))); + }); + + return clientWithIntercepter.GetContainer(databaseId, containerId); + } + + private static void ThrowTransportExceptionOnItemOperation( + Uri physicalAddress, + ResourceOperation resourceOperation, + DocumentServiceRequest request, + Guid activityId, + string transportExceptionSourceDescription) + { + if (request.ResourceType == ResourceType.Document) + { + TransportException transportException = new TransportException( + errorCode: TransportErrorCode.RequestTimeout, + innerException: null, + activityId: activityId, + requestUri: physicalAddress, + sourceDescription: transportExceptionSourceDescription, + userPayload: true, + payloadSent: false); + + throw Documents.Rntbd.TransportExceptions.GetRequestTimeoutException(physicalAddress, Guid.NewGuid(), + transportException); + } + } + + private sealed class TransportClientWrapper : TransportClient + { + private readonly TransportClient baseClient; + private readonly Action interceptor; + + internal TransportClientWrapper( + TransportClient client, + Action interceptor) + { + Debug.Assert(client != null); + Debug.Assert(interceptor != null); + + this.baseClient = client; + this.interceptor = interceptor; + } + + internal override async Task InvokeStoreAsync( + Uri physicalAddress, + ResourceOperation resourceOperation, + DocumentServiceRequest request) + { + this.interceptor(physicalAddress, resourceOperation, request); + + StoreResponse response = await this.baseClient.InvokeStoreAsync(physicalAddress, resourceOperation, request); + return response; + } + } + } +} diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/ChangeFeedResultSetIteratorTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/ChangeFeedResultSetIteratorTests.cs index 552a76ef45..3084d29794 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/ChangeFeedResultSetIteratorTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/ChangeFeedResultSetIteratorTests.cs @@ -13,6 +13,7 @@ namespace Microsoft.Azure.Cosmos.Tests using System.Threading.Tasks; using Microsoft.Azure.Cosmos.Query; using Microsoft.Azure.Cosmos.Query.Core.ContinuationTokens; + using Microsoft.Azure.Cosmos.Resource.CosmosExceptions; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; using Newtonsoft.Json; @@ -34,9 +35,15 @@ public async Task ContinuationTokenIsNotUpdatedOnFails() ResponseMessage firstResponse = new ResponseMessage(HttpStatusCode.NotModified); firstResponse.Headers.ETag = "FirstContinuation"; - ResponseMessage secondResponse = new ResponseMessage(HttpStatusCode.NotFound); - secondResponse.Headers.ETag = "ShouldNotContainThis"; - secondResponse.ErrorMessage = "something"; + ResponseMessage secondResponse = new ResponseMessage( + statusCode: HttpStatusCode.NotFound, + requestMessage: null, + headers: new Headers() + { + ETag = "ShouldNotContainThis" + }, + cosmosException: CosmosExceptionFactory.CreateNotFoundException("something"), + diagnostics: CosmosDiagnosticsContext.Create()); mockContext.SetupSequence(x => x.ProcessResourceOperationAsync( It.IsAny(), diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosExceptionTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosExceptionTests.cs index 7b0b58af51..e265ede0cc 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosExceptionTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosExceptionTests.cs @@ -5,6 +5,7 @@ namespace Microsoft.Azure.Cosmos { using System; + using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; @@ -12,6 +13,7 @@ namespace Microsoft.Azure.Cosmos using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Cosmos.Common; + using Microsoft.Azure.Cosmos.Resource.CosmosExceptions; using Microsoft.Azure.Cosmos.Routing; using Microsoft.Azure.Documents; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -111,5 +113,133 @@ public void VerifyDocumentClientExceptionToResponseMessage() Assert.IsTrue(responseMessage.ErrorMessage.Contains(errorMessage)); Assert.IsTrue(responseMessage.ErrorMessage.Contains("VerifyDocumentClientExceptionToResponseMessage"), $"Message should have method name for the stack trace {responseMessage.ErrorMessage}"); } + + [TestMethod] + public void VerifyTransportExceptionToResponseMessage() + { + string errorMessage = "Test Exception!"; + DocumentClientException dce = null; + TransportException transportException = new TransportException( + errorCode: TransportErrorCode.ConnectionBroken, + innerException: null, + activityId: Guid.NewGuid(), + requestUri: new Uri("https://localhost"), + sourceDescription: "The SourceDescription", + userPayload: true, + payloadSent: true); + + try + { + throw new ServiceUnavailableException( + message: errorMessage, + innerException: transportException); + } + catch (DocumentClientException exception) + { + dce = exception; + } + + ResponseMessage responseMessage = dce.ToCosmosResponseMessage(null); + Assert.IsFalse(responseMessage.IsSuccessStatusCode); + Assert.AreEqual(HttpStatusCode.ServiceUnavailable, responseMessage.StatusCode); + Assert.IsTrue(responseMessage.ErrorMessage.Contains(errorMessage)); + Assert.IsTrue(responseMessage.ErrorMessage.Contains(transportException.ToString())); + Assert.IsTrue(responseMessage.ErrorMessage.Contains("VerifyTransportExceptionToResponseMessage"), $"Message should have method name for the stack trace {responseMessage.ErrorMessage}"); + } + + [TestMethod] + public void EnsureCorrectStatusCode() + { + string testMessage = "Test" + Guid.NewGuid().ToString(); + + List<(HttpStatusCode statusCode, CosmosException exception)> exceptionsToStatusCodes = new List<(HttpStatusCode, CosmosException)>() + { + (HttpStatusCode.NotFound, CosmosExceptionFactory.CreateNotFoundException(testMessage)), + (HttpStatusCode.InternalServerError, CosmosExceptionFactory.CreateInternalServerErrorException(testMessage)), + (HttpStatusCode.BadRequest, CosmosExceptionFactory.CreateBadRequestException(testMessage)), + (HttpStatusCode.RequestTimeout,CosmosExceptionFactory.CreateRequestTimeoutException(testMessage)), + ((HttpStatusCode)429, CosmosExceptionFactory.CreateThrottledException(testMessage)), + }; + + foreach((HttpStatusCode statusCode, CosmosException exception) item in exceptionsToStatusCodes) + { + this.ValidateExceptionInfo(item.exception, item.statusCode, testMessage); + } + } + + [TestMethod] + public void ValidateExceptionStackTraceHandling() + { + CosmosException cosmosException = CosmosExceptionFactory.CreateNotFoundException("TestMessage"); + Assert.AreEqual(null, cosmosException.StackTrace); + Assert.IsFalse(cosmosException.ToString().Contains(nameof(ValidateExceptionStackTraceHandling))); + try + { + throw cosmosException; + } + catch(CosmosException ce) + { + Assert.IsTrue(ce.StackTrace.Contains(nameof(ValidateExceptionStackTraceHandling)), ce.StackTrace); + } + + string stackTrace = "OriginalDocumentClientExceptionStackTrace"; + try + { + throw CosmosExceptionFactory.CreateNotFoundException("TestMessage", stackTrace: stackTrace); + } + catch (CosmosException ce) + { + Assert.AreEqual(stackTrace, ce.StackTrace); + } + } + + [TestMethod] + public void ValidateErrorHandling() + { + Error error = new Error() + { + Code = System.Net.HttpStatusCode.BadRequest.ToString(), + Message = "Unsupported Query", + AdditionalErrorInfo = "Additional error info message" + }; + + CosmosDiagnosticsContext diagnostics = new CosmosDiagnosticsContextCore(); + + CosmosException cosmosException = CosmosExceptionFactory.CreateBadRequestException( + null, + error: error, + diagnosticsContext: diagnostics); + + ResponseMessage responseMessage = QueryResponse.CreateFailure( + statusCode: System.Net.HttpStatusCode.BadRequest, + cosmosException: cosmosException, + requestMessage: null, + diagnostics: diagnostics, + responseHeaders: null); + + Assert.AreEqual(error, responseMessage.CosmosException.Error); + Assert.IsTrue(responseMessage.ErrorMessage.Contains(error.Message)); + Assert.IsTrue(responseMessage.ErrorMessage.Contains(error.AdditionalErrorInfo)); + + try + { + responseMessage.EnsureSuccessStatusCode(); + Assert.Fail("Should throw exception"); + }catch(CosmosException ce ) when (ce.StatusCode == HttpStatusCode.BadRequest) + { + Assert.IsTrue(ce.Message.Contains(error.Message)); + Assert.IsTrue(ce.ToString().Contains(error.Message)); + Assert.IsTrue(ce.ToString().Contains(error.AdditionalErrorInfo)); + } + } + + private void ValidateExceptionInfo( + CosmosException exception, + HttpStatusCode httpStatusCode, + string message) + { + Assert.AreEqual(httpStatusCode, exception.StatusCode); + Assert.IsTrue(exception.ToString().Contains(message)); + } } } diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosQueryUnitTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosQueryUnitTests.cs index c0502d1f30..2291308714 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosQueryUnitTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/CosmosQueryUnitTests.cs @@ -24,6 +24,7 @@ namespace Microsoft.Azure.Cosmos.Tests using Microsoft.Azure.Cosmos.Query.Core.Monads; using Microsoft.Azure.Cosmos.Query.Core.QueryClient; using Microsoft.Azure.Cosmos.Query.Core.QueryPlan; + using Microsoft.Azure.Cosmos.Resource.CosmosExceptions; using Microsoft.Azure.Documents; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -38,13 +39,12 @@ public void VerifyNegativeCosmosQueryResponseStream() string errorMessage = "TestErrorMessage"; string activityId = "TestActivityId"; double requestCharge = 42.42; - + CosmosException cosmosException = CosmosExceptionFactory.CreateBadRequestException(errorMessage); CosmosDiagnosticsContext diagnostics = CosmosDiagnosticsContext.Create(); QueryResponse queryResponse = QueryResponse.CreateFailure( statusCode: HttpStatusCode.NotFound, - errorMessage: errorMessage, + cosmosException: cosmosException, requestMessage: null, - error: null, responseHeaders: new CosmosQueryResponseMessageHeaders( null, null, @@ -57,7 +57,7 @@ public void VerifyNegativeCosmosQueryResponseStream() diagnostics: diagnostics); Assert.AreEqual(HttpStatusCode.NotFound, queryResponse.StatusCode); - Assert.AreEqual(errorMessage, queryResponse.ErrorMessage); + Assert.AreEqual(cosmosException.ToString(includeDiagnostics: false), queryResponse.ErrorMessage); Assert.AreEqual(requestCharge, queryResponse.Headers.RequestCharge); Assert.AreEqual(activityId, queryResponse.Headers.ActivityId); Assert.AreEqual(diagnostics, queryResponse.Diagnostics); @@ -283,7 +283,7 @@ public async Task TestCosmosQueryPartitionKeyDefinition() QueryResponseCore queryResponse = await context.ExecuteNextAsync(cancellationtoken); Assert.AreEqual(HttpStatusCode.BadRequest, queryResponse.StatusCode); - Assert.IsTrue(queryResponse.ErrorMessage.Contains(exceptionMessage), "response error message did not contain the proper substring."); + Assert.IsTrue(queryResponse.CosmosException.ToString().Contains(exceptionMessage), "response error message did not contain the proper substring."); } private async Task<(IList components, QueryResponseCore response)> GetAllExecutionComponents() @@ -363,7 +363,18 @@ public async Task TestCosmosQueryPartitionKeyDefinition() QueryResponseCore failure = QueryResponseCore.CreateFailure( System.Net.HttpStatusCode.Unauthorized, SubStatusCodes.PartitionKeyMismatch, - "Random error message", + new CosmosException( + statusCodes: HttpStatusCode.Unauthorized, + message: "Random error message", + subStatusCode: default, + stackTrace: default, + activityId: "TestActivityId", + requestCharge: 42.89, + retryAfter: default, + headers: default, + diagnosticsContext: default, + error: default, + innerException: default), 42.89, "TestActivityId", diagnostics); diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/DotNetSDKAPI.json b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/DotNetSDKAPI.json index c455c8fbf1..88b93e2a55 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/DotNetSDKAPI.json +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/DotNetSDKAPI.json @@ -1756,11 +1756,9 @@ "Attributes": [], "MethodInfo": null }, - "Microsoft.Azure.Cosmos.CosmosDiagnostics get_Diagnostics()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { + "Microsoft.Azure.Cosmos.CosmosDiagnostics get_Diagnostics()": { "Type": "Method", - "Attributes": [ - "CompilerGeneratedAttribute" - ], + "Attributes": [], "MethodInfo": "Microsoft.Azure.Cosmos.CosmosDiagnostics get_Diagnostics()" }, "Microsoft.Azure.Cosmos.Headers get_Headers()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { @@ -1818,11 +1816,21 @@ ], "MethodInfo": "System.String get_ResponseBody()" }, + "System.String get_StackTrace()": { + "Type": "Method", + "Attributes": [], + "MethodInfo": "System.String get_StackTrace()" + }, "System.String ResponseBody": { "Type": "Property", "Attributes": [], "MethodInfo": null }, + "System.String StackTrace": { + "Type": "Property", + "Attributes": [], + "MethodInfo": null + }, "System.String ToString()": { "Type": "Method", "Attributes": [], @@ -5231,11 +5239,9 @@ "Attributes": [], "MethodInfo": "System.String get_ContinuationToken()" }, - "System.String get_ErrorMessage()[System.Runtime.CompilerServices.CompilerGeneratedAttribute()]": { + "System.String get_ErrorMessage()": { "Type": "Method", - "Attributes": [ - "CompilerGeneratedAttribute" - ], + "Attributes": [], "MethodInfo": "System.String get_ErrorMessage()" }, "Void .ctor()": { diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GlobalEndpointManagerTest.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GlobalEndpointManagerTest.cs index e1983f15df..b17f9ab481 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GlobalEndpointManagerTest.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/GlobalEndpointManagerTest.cs @@ -6,6 +6,7 @@ namespace Microsoft.Azure.Cosmos using System; using System.Collections.ObjectModel; using System.Threading; + using System.Threading.Tasks; using Microsoft.Azure.Cosmos.Routing; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -20,7 +21,7 @@ public class GlobalEndpointManagerTest /// Tests for /// [TestMethod] - public void EndpointFailureMockTest() + public async Task EndpointFailureMockTest() { // Setup dummpy read locations for the database account Collection readableLocations = new Collection(); @@ -56,7 +57,7 @@ public void EndpointFailureMockTest() GlobalEndpointManager globalEndpointManager = new GlobalEndpointManager(mockOwner.Object, connectionPolicy); - globalEndpointManager.RefreshLocationAsync(databaseAccount).Wait(); + await globalEndpointManager.RefreshLocationAsync(databaseAccount); Assert.AreEqual(globalEndpointManager.ReadEndpoints[0], new Uri(readLocation1.Endpoint)); //Mark each of the read locations as unavailable and validate that the read endpoint switches to the next preferred region / default endpoint. @@ -65,12 +66,12 @@ public void EndpointFailureMockTest() Assert.AreEqual(globalEndpointManager.ReadEndpoints[0], new Uri(readLocation2.Endpoint)); globalEndpointManager.MarkEndpointUnavailableForRead(globalEndpointManager.ReadEndpoints[0]); - globalEndpointManager.RefreshLocationAsync(null).Wait(); + await globalEndpointManager.RefreshLocationAsync(null); Assert.AreEqual(globalEndpointManager.ReadEndpoints[0], globalEndpointManager.WriteEndpoints[0]); //Sleep a second for the unavailable endpoint entry to expire and background refresh timer to kick in - Thread.Sleep(1000); - globalEndpointManager.RefreshLocationAsync(null).Wait(); + Thread.Sleep(2000); + await globalEndpointManager.RefreshLocationAsync(null); Assert.AreEqual(globalEndpointManager.ReadEndpoints[0], new Uri(readLocation1.Endpoint)); } diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/ItemProducerTreeUnitTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/ItemProducerTreeUnitTests.cs index c945786868..d813926cb6 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/ItemProducerTreeUnitTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/ItemProducerTreeUnitTests.cs @@ -19,6 +19,7 @@ namespace Microsoft.Azure.Cosmos.Tests using Microsoft.Azure.Cosmos.Query.Core.ExecutionContext.Parallel; using Microsoft.Azure.Cosmos.Query.Core.Metrics; using Microsoft.Azure.Cosmos.Query.Core.QueryClient; + using Microsoft.Azure.Cosmos.Resource.CosmosExceptions; using Microsoft.Azure.Documents; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -269,7 +270,8 @@ void produceAsyncCompleteCallback( Task.FromResult(QueryResponseCore.CreateFailure( statusCode: HttpStatusCode.InternalServerError, subStatusCodes: null, - errorMessage: "Error message", + cosmosException: CosmosExceptionFactory.CreateInternalServerErrorException( + "Error message"), requestCharge: 10.2, activityId: Guid.NewGuid().ToString(), diagnostics: pageDiagnostics))); diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/QueryPipelineMockTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/QueryPipelineMockTests.cs index 95f0be3908..06e7ead29d 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/QueryPipelineMockTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/QueryPipelineMockTests.cs @@ -212,7 +212,7 @@ public async Task TestCosmosCrossPartitionQueryExecutionContextWithFailuresAsync Assert.IsNotNull(failure); Assert.AreEqual((HttpStatusCode)429, failure.Value.StatusCode); - Assert.IsNull(failure.Value.ErrorMessage); + Assert.IsNotNull(failure.Value.CosmosException.ToString()); Assert.AreEqual(allItems.Count, itemsRead.Count); List exepected = allItems.OrderBy(x => x.id).ToList(); @@ -478,7 +478,7 @@ public async Task TestCosmosOrderByQueryExecutionContextWithFailurePageAsync(boo Assert.IsNotNull(failure); Assert.AreEqual((HttpStatusCode)429, failure.Value.StatusCode); - Assert.IsNull(failure.Value.ErrorMessage); + Assert.IsNotNull(failure.Value.CosmosException.ToString()); Assert.AreEqual(0 /*We don't get any items, since we don't buffer the failure anymore*/, itemsRead.Count); diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/QueryResponseFactoryTests.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/QueryResponseFactoryTests.cs index 9a64b5cd68..8d3734704f 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/QueryResponseFactoryTests.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/QueryResponseFactoryTests.cs @@ -6,10 +6,12 @@ namespace Microsoft.Azure.Cosmos.Query { using System; using System.Net; + using System.Runtime.CompilerServices; using Microsoft.Azure.Cosmos.Query.Core; using Microsoft.Azure.Cosmos.Query.Core.Exceptions; using Microsoft.Azure.Cosmos.Query.Core.Monads; using Microsoft.Azure.Cosmos.Query.Core.QueryClient; + using Microsoft.Azure.Cosmos.Resource.CosmosExceptions; using VisualStudio.TestTools.UnitTesting; [TestClass] @@ -18,12 +20,11 @@ public class QueryResponseFactoryTests [TestMethod] public void CosmosException() { - CosmosException cosmosException = new CosmosException( - statusCode: HttpStatusCode.BadRequest, + CosmosException cosmosException = CosmosExceptionFactory.CreateBadRequestException( message: "asdf"); QueryResponseCore queryResponse = QueryResponseFactory.CreateFromException(cosmosException); Assert.AreEqual(HttpStatusCode.BadRequest, queryResponse.StatusCode); - Assert.IsNotNull(queryResponse.ErrorMessage); + Assert.IsNotNull(queryResponse.CosmosException); } [TestMethod] @@ -32,7 +33,7 @@ public void DocumentClientException() Documents.DocumentClientException documentClientException = new Documents.RequestRateTooLargeException("asdf"); QueryResponseCore queryResponse = QueryResponseFactory.CreateFromException(documentClientException); Assert.AreEqual((HttpStatusCode)429, queryResponse.StatusCode); - Assert.IsNotNull(queryResponse.ErrorMessage); + Assert.IsNotNull(queryResponse.CosmosException); } [TestMethod] @@ -48,18 +49,48 @@ public void QueryException() QueryException queryException = new MalformedContinuationTokenException(); QueryResponseCore queryResponse = QueryResponseFactory.CreateFromException(queryException); Assert.AreEqual(HttpStatusCode.BadRequest, queryResponse.StatusCode); - Assert.IsNotNull(queryResponse.ErrorMessage); + Assert.IsNotNull(queryResponse.CosmosException); } [TestMethod] public void ExceptionFromTryCatch() { - QueryException queryException = new MalformedContinuationTokenException(); - TryCatch tryCatch = TryCatch.FromException(queryException); + TryCatch tryCatch = this.QueryExceptionHelper(new MalformedContinuationTokenException("TestMessage")); + QueryResponseCore queryResponse = QueryResponseFactory.CreateFromException(tryCatch.Exception); + Assert.AreEqual(HttpStatusCode.BadRequest, queryResponse.StatusCode); + Assert.IsNotNull(queryResponse.CosmosException); + Assert.IsTrue(queryResponse.CosmosException.ToString().Contains("TestMessage")); + Assert.IsTrue(queryResponse.CosmosException.ToString().Contains(nameof(QueryExceptionHelper)), queryResponse.CosmosException.ToString()); + } + + [TestMethod] + public void ExceptionFromTryCatchWithCosmosException() + { + CosmosException cosmosException; + try + { + throw CosmosExceptionFactory.CreateBadRequestException("InternalServerTestMessage"); + } + catch (CosmosException ce) + { + cosmosException = ce; + } + + TryCatch tryCatch = this.QueryExceptionHelper(cosmosException); QueryResponseCore queryResponse = QueryResponseFactory.CreateFromException(tryCatch.Exception); Assert.AreEqual(HttpStatusCode.BadRequest, queryResponse.StatusCode); - Assert.IsNotNull(queryResponse.ErrorMessage); - Assert.IsTrue(queryResponse.ErrorMessage.Contains(nameof(ExceptionFromTryCatch))); + Assert.IsNotNull(queryResponse.CosmosException); + + // Should preserve the original stack trace. + string exceptionMessage = queryResponse.CosmosException.ToString(); + Assert.IsTrue(exceptionMessage.Contains("InternalServerTestMessage")); + Assert.IsFalse(exceptionMessage.Contains(nameof(QueryExceptionHelper))); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private TryCatch QueryExceptionHelper(Exception exception) + { + return TryCatch.FromException(exception); } } } diff --git a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/QueryResponseMessageFactory.cs b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/QueryResponseMessageFactory.cs index 06ca77ca80..ddcbf71b93 100644 --- a/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/QueryResponseMessageFactory.cs +++ b/Microsoft.Azure.Cosmos/tests/Microsoft.Azure.Cosmos.Tests/Query/QueryResponseMessageFactory.cs @@ -15,6 +15,7 @@ namespace Microsoft.Azure.Cosmos.Tests using Microsoft.Azure.Cosmos.Json; using Microsoft.Azure.Cosmos.Query.Core.Metrics; using Microsoft.Azure.Cosmos.Query.Core.QueryClient; + using Microsoft.Azure.Cosmos.Resource.CosmosExceptions; using Microsoft.Azure.Documents; internal static class QueryResponseMessageFactory @@ -87,7 +88,7 @@ public static QueryResponseCore CreateQueryResponse( if (isOrderByQuery) { memoryStream = SerializeForOrderByQuery(items); - using(StreamReader sr = new StreamReader(SerializeForOrderByQuery(items))) + using (StreamReader sr = new StreamReader(SerializeForOrderByQuery(items))) { json = sr.ReadToEnd(); } @@ -146,9 +147,10 @@ public static QueryResponseCore CreateFailureResponse( SubStatusCodes subStatusCodes, string errorMessage) { + string acitivityId = Guid.NewGuid().ToString(); CosmosDiagnosticsContext diagnosticsContext = CosmosDiagnosticsContext.Create(); diagnosticsContext.AddDiagnosticsInternal(new PointOperationStatistics( - Guid.NewGuid().ToString(), + acitivityId, System.Net.HttpStatusCode.Gone, subStatusCode: SubStatusCodes.PartitionKeyRangeGone, requestCharge: 10.4, @@ -170,9 +172,20 @@ public static QueryResponseCore CreateFailureResponse( QueryResponseCore splitResponse = QueryResponseCore.CreateFailure( statusCode: httpStatusCode, subStatusCodes: subStatusCodes, - errorMessage: errorMessage, + cosmosException: CosmosExceptionFactory.Create( + statusCode: httpStatusCode, + subStatusCode: (int)subStatusCodes, + message: errorMessage, + stackTrace: new System.Diagnostics.StackTrace().ToString(), + activityId: acitivityId, + requestCharge: 10.4, + retryAfter: default, + headers: default, + diagnosticsContext: diagnosticsContext, + error: default, + innerException: default), requestCharge: 10.4, - activityId: Guid.NewGuid().ToString(), + activityId: acitivityId, diagnostics: diagnostics); return splitResponse; diff --git a/changelog.md b/changelog.md index ed07a36d32..d42d318923 100644 --- a/changelog.md +++ b/changelog.md @@ -11,6 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- [#1213](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/1213) CosmosException now returns the original stack trace. +- [#1213](https://github.com/Azure/azure-cosmos-dotnet-v3/pull/1213) ResponseMessage.ErrorMessage is now always correctly populated. There was bug in some scenarios where the error message was left in the content stream. + ## [3.7.0-preview](https://www.nuget.org/packages/Microsoft.Azure.Cosmos/3.7.0-preview) - 2020-02-25 - [#1210](hhttps://github.com/Azure/azure-cosmos-dotnet-v3/pull/1210) Change Feed pull model