diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-error-handling/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-error-handling/_index.md new file mode 100644 index 000000000..fa87e2aac --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-error-handling/_index.md @@ -0,0 +1,7 @@ +--- +type: docs +title: "Error Handling in the Dapr .NET SDK" +linkTitle: "Error handling" +weight: 90000 +description: Learn about error handling in the Dapr.NET SDK. +--- \ No newline at end of file diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-error-handling/dotnet-richer-error-model.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-error-handling/dotnet-richer-error-model.md new file mode 100644 index 000000000..5efd11fca --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-error-handling/dotnet-richer-error-model.md @@ -0,0 +1,140 @@ +--- +type: docs +title: "Richer Error Model in the Dapr .NET SDK" +linkTitle: "Richer error model" +weight: 59000 +description: Learn how to use the richer error model in the .NET SDK. +--- + +The Dapr .NET SDK supports the richer error model, implemented by the Dapr runtime. This model provides a way for applications to enrich their errors with added context, +allowing consumers of the application to better understand the issue and resolve faster. You can read more about the richer error model [here](https://google.aip.dev/193), and you +can find the Dapr proto file implementing these errors [here](https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto"). + +The Dapr .NET SDK implements all details supported by the Dapr runtime, implemented in the `Dapr.Common.Exceptions` namespace, and is accessible through +the `DaprException` extension method `TryGetExtendedErrorInfo`. Currently this detail extraction is only supported for +`RpcException`'s where the details are present. + +```csharp +// Example usage of ExtendedErrorInfo + +try +{ + // Perform some action with the Dapr client that throws a DaprException. +} +catch (DaprException daprEx) +{ + if (daprEx.TryGetExtendedErrorInfo(out DaprExtendedErrorInfo errorInfo) + { + Console.WriteLine(errorInfo.Code); + Console.WriteLine(errorInfo.Message); + + foreach (DaprExtendedErrorDetail detail in errorInfo.Details) + { + Console.WriteLine(detail.ErrorType); + switch (detail.ErrorType) + case ExtendedErrorType.ErrorInfo: + Console.WriteLine(detail.Reason); + Console.WriteLine(detail.Domain); + default: + Console.WriteLine(detail.TypeUrl); + } + } +} +``` + +## DaprExtendedErrorInfo + +Contains `Code` (the status code) and `Message` (the error message) associated with the error, parsed from an inner `RpcException`. +Also contains a collection of `DaprExtendedErrorDetails` parsed from the details in the exception. + +## DaprExtendedErrorDetail + +All details implement the abstract `DaprExtendedErrorDetail` and have an associated `DaprExtendedErrorType`. + +1. [RetryInfo](#retryinfo) + +2. [DebugInfo](#debuginfo) + +3. [QuotaFailure](#quotafailure) + +4. [PreconditionFailure](#preconditionfailure) + +5. [RequestInfo](#requestinfo) + +6. [LocalizedMessage](#localizedmessage) + +7. [BadRequest](#badrequest) + +8. [ErrorInfo](#errorinfo) + +9. [Help](#help) + +10. [ResourceInfo](#resourceinfo) + +11. [Unknown](#unknown) + +## RetryInfo + +Information telling the client how long to wait before they should retry. Provides a `DaprRetryDelay` with the properties +`Second` (offset in seconds) and `Nano` (offset in nanoseconds). + +## DebugInfo + +Debugging information offered by the server. Contains `StackEntries` (a collection of strings containing the stack trace), and +`Detail` (further debugging information). + +## QuotaFailure + +Information relating to some quota that may have been reached, such as a daily usage limit on an API. It has one property `Violations`, +a collection of `DaprQuotaFailureViolation`, which each contain a `Subject` (the subject of the request) and `Description` (further information regarding the failure). + +## PreconditionFailure + +Information informing the client that some required precondition was not met. Has one property `Violations`, a collection of +`DaprPreconditionFailureViolation`, which each has `Subject` (subject where the precondition failure occured e.g. "Azure"), `Type` (representation of the precondition type e.g. "TermsOfService"), and `Description` (further description e.g. "ToS must be accepted."). + +## RequestInfo + +Information returned by the server that can be used by the server to identify the clients request. Contains +`RequestId` and `ServingData` properties, `RequestId` being some string (such as a UID) the server can interpret, +and `ServingData` being some arbitrary data that made up part of the request. + +## LocalizedMessage + +Contains a localized message, along with the locale of the message. Contains `Locale` (the locale e.g. "en-US") and `Message` (the localized message). + +## BadRequest + +Describes a bad request field. Contains collection of `DaprBadRequestDetailFieldViolation`, which each has `Field` (the offending field in request e.g. 'first_name') and +`Description` (further information detailing the reason e.g. "first_name cannot contain special characters"). + +## ErrorInfo + +Details the cause of an error. Contains three properties, `Reason` (the reason for the error, which should take the form of UPPER_SNAKE_CASE e.g. DAPR_INVALID_KEY), +`Domain` (domain the error belongs to e.g. 'dapr.io'), and `Metadata`, a key value based collection of futher information. + +## Help + +Provides resources for the client to perform further research into the issue. Contains a collection of `DaprHelpDetailLink`, +which provides `Url` (a url to help or documentation), and `Description` (a description of what the link provides). + +## ResourceInfo + +Provides information relating to an accessed resource. Provides three properties `ResourceType` (type of the resource being access e.g. "Azure service bus"), +`ResourceName` (The name of the resource e.g. "my-configured-service-bus"), `Owner` (the owner of the resource e.g. "subscriptionowner@dapr.io"), +and `Description` (further information on the resource relating to the error e.g. "missing permissions to use this resource"). + +## Unknown + +Returned when the detail type url cannot be mapped to the correct `DaprExtendedErrorDetail` implementation. +Provides one property `TypeUrl` (the type url that could not be parsed e.g. "type.googleapis.com/Google.rpc.UnrecognizedType"). + + + + + + + + + + diff --git a/src/Dapr.Common/Dapr.Common.csproj b/src/Dapr.Common/Dapr.Common.csproj index d1e106b6d..dd34d844b 100644 --- a/src/Dapr.Common/Dapr.Common.csproj +++ b/src/Dapr.Common/Dapr.Common.csproj @@ -7,6 +7,7 @@ + diff --git a/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs b/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs new file mode 100644 index 000000000..2f195d692 --- /dev/null +++ b/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs @@ -0,0 +1,55 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Diagnostics.CodeAnalysis; +using Grpc.Core; + +namespace Dapr.Common.Exceptions +{ + /// + /// Provides extension methods for . + /// + public static class DaprExceptionExtensions + { + /// + /// Attempt to retrieve from . + /// + /// A Dapr exception. . + /// out if parsable from inner exception, null otherwise. + /// True if extended info is available, false otherwise. + public static bool TryGetExtendedErrorInfo(this DaprException exception, [NotNullWhen(true)] out DaprExtendedErrorInfo? daprExtendedErrorInfo) + { + daprExtendedErrorInfo = null; + if (exception.InnerException is not RpcException rpcException) + { + return false; + } + + var metadata = rpcException.Trailers.Get(DaprExtendedErrorConstants.GrpcDetails); + + if (metadata is null) + { + return false; + } + + var status = Google.Rpc.Status.Parser.ParseFrom(metadata.ValueBytes); + + daprExtendedErrorInfo = new DaprExtendedErrorInfo(status.Code, status.Message) + { + Details = status.Details.Select(detail => ExtendedErrorDetailFactory.CreateErrorDetail(detail)).ToArray(), + }; + + return true; + } + } +} diff --git a/src/Dapr.Common/Exceptions/DaprExtendedErrorConstants.cs b/src/Dapr.Common/Exceptions/DaprExtendedErrorConstants.cs new file mode 100644 index 000000000..9507103bb --- /dev/null +++ b/src/Dapr.Common/Exceptions/DaprExtendedErrorConstants.cs @@ -0,0 +1,34 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Common.Exceptions +{ + /// + /// Definitions of expected types to be returned from the Dapr runtime. + /// + internal static class DaprExtendedErrorConstants + { + public const string ErrorDetailTypeUrl = "type.googleapis.com/"; + public const string GrpcDetails = "grpc-status-details-bin"; + public const string ErrorInfo = $"{ErrorDetailTypeUrl}Google.rpc.ErrorInfo"; + public const string RetryInfo = $"{ErrorDetailTypeUrl}Google.rpc.RetryInfo"; + public const string DebugInfo = $"{ErrorDetailTypeUrl}Google.rpc.DebugInfo"; + public const string QuotaFailure = $"{ErrorDetailTypeUrl}Google.rpc.QuotaFailure"; + public const string PreconditionFailure = $"{ErrorDetailTypeUrl}Google.rpc.PreconditionFailure"; + public const string BadRequest = $"{ErrorDetailTypeUrl}Google.rpc.BadRequest"; + public const string RequestInfo = $"{ErrorDetailTypeUrl}Google.rpc.RequestInfo"; + public const string ResourceInfo = $"{ErrorDetailTypeUrl}Google.rpc.ResourceInfo"; + public const string Help = $"{ErrorDetailTypeUrl}Google.rpc.Help"; + public const string LocalizedMessage = $"{ErrorDetailTypeUrl}Google.rpc.LocalizedMessage"; + } +} diff --git a/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs b/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs new file mode 100644 index 000000000..17c10c20b --- /dev/null +++ b/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs @@ -0,0 +1,157 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Common.Exceptions +{ + /// + /// Abstract base class of the Dapr extended error detail. + /// + public abstract record DaprExtendedErrorDetail(DaprExtendedErrorType ErrorType); + + /// + /// Detail when the type url is unrecognized. + /// + /// The unrecognized type url. + public sealed record DaprUnknownDetail(string TypeUrl) : DaprExtendedErrorDetail(DaprExtendedErrorType.Unknown); + + /// + /// Detail proving debugging information. + /// + /// Stack trace entries relating to error. + /// Further related debugging information. + public sealed record DaprDebugInfoDetail(IReadOnlyCollection StackEntries, string Detail) : DaprExtendedErrorDetail(DaprExtendedErrorType.DebugInfo); + + /// + /// A precondtion violation. + /// + /// The type of the violation. + /// The subject that the violation relates to. + /// A description of how the precondition may have failed. + public sealed record DaprPreconditionFailureViolation(string Type, string Subject, string Description); + + /// + /// Detail relating to a failed precondition e.g user has not completed some required check. + /// + public sealed record DaprPreconditionFailureDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.PreconditionFailure) + { + /// + /// Collection of . + /// + public IReadOnlyCollection Violations { get; init; } = Array.Empty(); + } + + /// + /// Provides the time offset the client should use before retrying. + /// + /// Second offset. + /// Nano offset. + public sealed record DaprRetryDelay(long Seconds, int Nanos); + + /// + /// Detail containing retry information. Provides the minimum amount of the time the client should wait before retrying a request. + /// + public sealed record DaprRetryInfoDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.RetryInfo) + { + /// + /// A . + /// + public DaprRetryDelay Delay = new(Seconds: 1, Nanos: default); + } + + /// + /// Further details relating to a quota violation. + /// + /// The subject where the quota violation occured e.g and ip address or remote resource. + /// Further information relating to the quota violation. + public sealed record DaprQuotaFailureViolation(string Subject, string Description); + + /// + /// Detail relating to a quota failure e.g reaching API limit. + /// + public sealed record DaprQuotaFailureDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.QuotaFailure) + { + /// + /// Collection of . + /// + public IReadOnlyCollection Violations { get; init; } = Array.Empty(); + } + + /// + /// Further infomation related to a bad request. + /// + /// The field that generated the bad request e.g 'NewAccountName||'. + /// Further description of the field error e.g 'Account name cannot contain '||'' + public sealed record DaprBadRequestDetailFieldViolation(string Field, string Description); + + /// + /// Detail containing information related to a bad request from the client. + /// + public sealed record DaprBadRequestDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.BadRequest) + { + /// + /// Collection of . + /// + public IReadOnlyCollection FieldViolations { get; init; } = Array.Empty(); + } + + /// + /// Detail containing request info used by the client to provide back to the server in relation to filing bugs, providing feedback, or general debugging by the server. + /// + /// A string understandable by the server e.g an internal UID related a trace. + /// Any data that furthers server debugging. + public sealed record DaprRequestInfoDetail(string RequestId, string ServingData) : DaprExtendedErrorDetail(DaprExtendedErrorType.RequestInfo); + + /// + /// Detail containing a message that can be localized. + /// + /// The locale e.g 'en-US'. + /// A message to be localized. + public sealed record DaprLocalizedMessageDetail(string Locale, string Message) : DaprExtendedErrorDetail(DaprExtendedErrorType.LocalizedMessage); + + + /// + /// Contains a link to a help resource. + /// + /// Url to help resources or documentation e.g 'https://v1-15.docs.dapr.io/developing-applications/error-codes/error-codes-reference/'. + /// A description of the link. + public sealed record DaprHelpDetailLink(string Url, string Description); + + /// + /// Detail containing links to further help resources. + /// + public sealed record DaprHelpDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.Help) + { + /// + /// Collection of . + /// + public IReadOnlyCollection Links { get; init; } = Array.Empty(); + } + + /// + /// Detail containg resource information. + /// + /// The type of the resource e.g 'state'. + /// The name of the resource e.g 'statestore'. + /// The owner of the resource. + /// Further description of the resource. + public sealed record DaprResourceInfoDetail(string ResourceType, string ResourceName, string Owner, string Description) : DaprExtendedErrorDetail(DaprExtendedErrorType.ResourceInfo); + + /// + /// Detail containing information related to a server error. + /// + /// The error reason e.g 'DAPR_STATE_ILLEGAL_KEY'. + /// The error domain e.g 'dapr.io'. + /// Further key / value based metadata. + public sealed record DaprErrorInfoDetail(string Reason, string Domain, IDictionary? Metadata) : DaprExtendedErrorDetail(DaprExtendedErrorType.ErrorInfo); + +} diff --git a/src/Dapr.Common/Exceptions/DaprExtendedErrorInfo.cs b/src/Dapr.Common/Exceptions/DaprExtendedErrorInfo.cs new file mode 100644 index 000000000..bcd337a38 --- /dev/null +++ b/src/Dapr.Common/Exceptions/DaprExtendedErrorInfo.cs @@ -0,0 +1,28 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Common.Exceptions +{ + /// + /// Dapr implementation of the richer error model. + /// + /// A status code. + /// A message. + public sealed record DaprExtendedErrorInfo(int Code, string Message) + { + /// + /// A collection of details that provide more information on the error. + /// + public DaprExtendedErrorDetail[] Details { get; init; } = Array.Empty(); + } +} diff --git a/src/Dapr.Common/Exceptions/DaprExtendedErrorType.cs b/src/Dapr.Common/Exceptions/DaprExtendedErrorType.cs new file mode 100644 index 000000000..664f6beef --- /dev/null +++ b/src/Dapr.Common/Exceptions/DaprExtendedErrorType.cs @@ -0,0 +1,90 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Common.Exceptions +{ + /// + /// Extended error detail types. + /// This is based on the Richer Error Model (see and + /// ) + /// and is implemented by the Dapr runtime (see ). + /// + public enum DaprExtendedErrorType + { + /// + /// Unknown extended error type. + /// Implemented by . + /// + Unknown, + + /// + /// Retry info detail type. + /// See . + /// + RetryInfo, + + /// + /// Debug info detail type. + /// See . + /// + DebugInfo, + + /// + /// Quote failure detail type. + /// See . + /// + QuotaFailure, + + /// + /// Precondition failure detail type. + /// See . + /// + PreconditionFailure, + + /// + /// Request info detail type. + /// See . + /// + RequestInfo, + + /// + /// Localized message detail type. + /// See . + /// + LocalizedMessage, + + /// + /// Bad request detail type. + /// See . + /// + BadRequest, + + /// + /// Error info detail type. + /// See . + /// + ErrorInfo, + + /// + /// Help detail type. + /// See . + /// + Help, + + /// + /// Resource info detail type. + /// See . + /// + ResourceInfo + } +} diff --git a/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs b/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs new file mode 100644 index 000000000..f81b6646d --- /dev/null +++ b/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs @@ -0,0 +1,122 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; +using Google.Rpc; + +namespace Dapr.Common.Exceptions +{ + /// + /// factory. + /// + internal static class ExtendedErrorDetailFactory + { + /// + /// Create a new from an instance of . + /// + /// The serialized detail message to create the error detail from. + /// A new instance of + internal static DaprExtendedErrorDetail CreateErrorDetail(Any message) + { + var data = message.Value; + return message.TypeUrl switch + { + DaprExtendedErrorConstants.RetryInfo => ToDaprRetryInfoDetail(data), + DaprExtendedErrorConstants.ErrorInfo => ToDaprErrorInfoDetail(data), + DaprExtendedErrorConstants.DebugInfo => ToDaprDebugInfoDetail(data), + DaprExtendedErrorConstants.QuotaFailure => ToDaprQuotaFailureDetail(data), + DaprExtendedErrorConstants.PreconditionFailure => ToDaprPreconditionFailureDetail(data), + DaprExtendedErrorConstants.BadRequest => ToDaprBadRequestDetail(data), + DaprExtendedErrorConstants.RequestInfo => ToDaprRequestInfoDetail(data), + DaprExtendedErrorConstants.ResourceInfo => ToDaprResourceInfoDetail(data), + DaprExtendedErrorConstants.Help => ToDaprHelpDetail(data), + DaprExtendedErrorConstants.LocalizedMessage => ToDaprLocalizedMessageDetail(data), + _ => new DaprUnknownDetail(message.TypeUrl) + }; + } + + private static DaprRetryInfoDetail ToDaprRetryInfoDetail(ByteString data) + { + var retryInfo = RetryInfo.Parser.ParseFrom(data); + return new() { Delay = new DaprRetryDelay(Seconds: retryInfo.RetryDelay.Seconds, Nanos: retryInfo.RetryDelay.Nanos) } ; + } + + private static DaprLocalizedMessageDetail ToDaprLocalizedMessageDetail(ByteString data) + { + var localizedMessage = LocalizedMessage.Parser.ParseFrom(data); + return new(Locale: localizedMessage.Locale, Message: localizedMessage.Message); + } + + private static DaprDebugInfoDetail ToDaprDebugInfoDetail(ByteString data) + { + var debugInfo = DebugInfo.Parser.ParseFrom(data); + return new(StackEntries: debugInfo.StackEntries.ToArray(), Detail: debugInfo.Detail); + } + + private static DaprQuotaFailureDetail ToDaprQuotaFailureDetail(ByteString data) + { + var quotaFailure = QuotaFailure.Parser.ParseFrom(data); + return new() + { + Violations = quotaFailure.Violations.Select(violation => new DaprQuotaFailureViolation(Subject: violation.Subject, Description: violation.Description)).ToArray(), + }; + } + + private static DaprPreconditionFailureDetail ToDaprPreconditionFailureDetail(ByteString data) + { + var preconditionFailure = PreconditionFailure.Parser.ParseFrom(data); + return new() + { + Violations = preconditionFailure.Violations.Select(violation => new DaprPreconditionFailureViolation(Type: violation.Type, Subject: violation.Subject, Description: violation.Description)).ToArray() + }; + } + + private static DaprRequestInfoDetail ToDaprRequestInfoDetail(ByteString data) + { + var requestInfo = RequestInfo.Parser.ParseFrom(data); + return new(RequestId: requestInfo.RequestId, ServingData: requestInfo.ServingData); + } + + private static DaprResourceInfoDetail ToDaprResourceInfoDetail(ByteString data) + { + var resourceInfo = ResourceInfo.Parser.ParseFrom(data); + return new(ResourceType: resourceInfo.ResourceType, ResourceName: resourceInfo.ResourceName, Owner: resourceInfo.Owner, Description: resourceInfo.Description); + } + + private static DaprBadRequestDetail ToDaprBadRequestDetail(ByteString data) + { + var badRequest = BadRequest.Parser.ParseFrom(data); + return new() + { + FieldViolations = badRequest.FieldViolations.Select( + fieldViolation => new DaprBadRequestDetailFieldViolation(Field: fieldViolation.Field, Description: fieldViolation.Description)).ToArray() + }; + } + + private static DaprErrorInfoDetail ToDaprErrorInfoDetail(ByteString data) + { + var errorInfo = ErrorInfo.Parser.ParseFrom(data); + return new(Reason: errorInfo.Reason, Domain: errorInfo.Domain, Metadata: errorInfo.Metadata); + } + + private static DaprHelpDetail ToDaprHelpDetail(ByteString data) + { + var helpInfo = Help.Parser.ParseFrom(data); + return new() + { + Links = helpInfo.Links.Select(link => new DaprHelpDetailLink(Url: link.Url, Description: link.Description)).ToArray() + }; + } + } +} diff --git a/test/Dapr.Common.Test/DaprExtendedErrorInfoTest.cs b/test/Dapr.Common.Test/DaprExtendedErrorInfoTest.cs new file mode 100644 index 000000000..b215bb600 --- /dev/null +++ b/test/Dapr.Common.Test/DaprExtendedErrorInfoTest.cs @@ -0,0 +1,690 @@ +using System.Collections.Generic; +using Dapr.Common.Exceptions; +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; +using Google.Rpc; +using Grpc.Core; +using Xunit; + +namespace Dapr.Common.Test +{ + public class DaprExtendedErrorInfoTest + { + private static int statusCode = 1; + private static string statusMessage = "Status Message"; + + [Fact] + public void DaprExendedErrorInfo_ThrowsRpcDaprException_ExtendedErrorInfoReturnsTrueAndNotNull() + { + // Arrange + DaprExtendedErrorInfo result = null; + var metadataEntry = new Google.Rpc.Status() + { + Code = statusCode, + Message = statusMessage, + }; + + BadRequest badRequest = new(); + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.BadRequest", Value = badRequest.ToByteString() }); + + Metadata trailers = new() + { + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } + }; + + var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "BadRequest"), trailers: trailers); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.NotNull(result); + } + + [Fact] + public void DaprExendedErrorInfo_ThrowsNonRpcDaprException_ExtendedErrorInfoReturnsFalseAndIsNull() + { + // Arrange + DaprExtendedErrorInfo result = null; + + // Act, Assert + try + { + throw new DaprException("Non-Rpc based Dapr exception"); + } + + catch (DaprException daprEx) + { + Assert.False(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.Null(result); + } + + [Fact] + public void DaprExendedErrorInfo_ThrowsRpcDaprException_NoTrailers_ExtendedErrorInfoIsNull() + { + // Arrange + DaprExtendedErrorInfo result = null; + + var rpcEx = new RpcException(status: new Grpc.Core.Status()); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.False(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.Null(result); + } + + [Fact] + public void DaprExtendedErrorInfo_ThrowsBadRequestRpcException_ShouldGetSingleDaprBadRequestDetail() + { + // Arrange + DaprExtendedErrorInfo result = null; + var metadataEntry = new Google.Rpc.Status() + { + Code = statusCode, + Message = statusMessage, + }; + + BadRequest badRequest = new(); + + var violationDescription = "Violation Description"; + var violationField = "Violation Field"; + + badRequest.FieldViolations.Add(new BadRequest.Types.FieldViolation() + { + Description = violationDescription, + Field = violationField, + }); + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.BadRequest", Value = badRequest.ToByteString() }); + + Metadata trailers = new() + { + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } + }; + + var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "BadRequest"), trailers: trailers); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.NotNull(result); + var detail = Assert.Single(result.Details); + + Assert.Equal(DaprExtendedErrorType.BadRequest, detail.ErrorType); + var badRequestDetail = Assert.IsType(detail); + + var violation = Assert.Single(badRequestDetail.FieldViolations); + + Assert.Equal(violation.Description, violationDescription); + Assert.Equal(violation.Field, violationField); + } + + [Fact] + public void DaprExtendedErrorInfo_ThrowsLocalizedMessageRpcException_ShouldGetSingleDaprLocalizedMessageDetail() + { + // Arrange + DaprExtendedErrorInfo result = null; + var metadataEntry = new Google.Rpc.Status() + { + Code = statusCode, + Message = statusMessage, + }; + + var localizedMessage = "Localized Message"; + var locale = "locale"; + + LocalizedMessage badRequest = new() + { + Message = localizedMessage, + Locale = locale, + }; + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.LocalizedMessage", Value = badRequest.ToByteString() }); + + Metadata trailers = new() + { + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } + }; + + var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "BadRequest"), trailers: trailers); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.NotNull(result); + var detail = Assert.Single(result.Details); + Assert.Equal(DaprExtendedErrorType.LocalizedMessage, detail.ErrorType); + + var localizedMessageDetail = Assert.IsType(detail); + + Assert.Equal(localizedMessage, localizedMessageDetail.Message); + Assert.Equal(locale, localizedMessageDetail.Locale); + } + + [Fact] + public void DaprExtendedErrorInfo_ThrowRetryInfoRpcException_ShouldGetSingleDaprRetryInfoDetail() + { + // Arrange + DaprExtendedErrorInfo result = null; + var metadataEntry = new Google.Rpc.Status() + { + Code = statusCode, + Message = statusMessage, + }; + + RetryInfo retryInfo = new(); + + retryInfo.RetryDelay = new Duration() + { + Seconds = 1, + Nanos = 0, + }; + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.RetryInfo", Value = retryInfo.ToByteString() }); + + Metadata trailers = new() + { + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } + }; + + var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.FailedPrecondition, "RetryInfo"), trailers: trailers); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.NotNull(result); + var detail = Assert.Single(result.Details); + Assert.Equal(DaprExtendedErrorType.RetryInfo, detail.ErrorType); + + var retryInfoDetail = Assert.IsType(detail); + + Assert.NotNull(retryInfoDetail); + Assert.Equal(1, retryInfoDetail.Delay.Seconds); + Assert.Equal(0, retryInfoDetail.Delay.Nanos); + } + + [Fact] + public void DaprExtendedErrorInfo_ThrowsDebugInfoRpcException_ShouldGetSingleDaprDebugInfoDetail() + { + // Arrange + DaprExtendedErrorInfo result = null; + var metadataEntry = new Google.Rpc.Status() + { + Code = statusCode, + Message = statusMessage, + }; + + Google.Rpc.DebugInfo debugInfo = new(); + + debugInfo.Detail = "Debug Detail"; + debugInfo.StackEntries.Add("Stack Entry"); + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.DebugInfo", Value = debugInfo.ToByteString() }); + + Grpc.Core.Metadata trailers = new() + { + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } + }; + + var rpcEx = new RpcException(status: new Grpc.Core.Status(Grpc.Core.StatusCode.FailedPrecondition, "DebugInfo"), trailers: trailers); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.NotNull(result); + var detail = Assert.Single(result.Details); + + Assert.Equal(DaprExtendedErrorType.DebugInfo, detail.ErrorType); + + var daprDebugInfoDetail = Assert.IsType(detail); + + Assert.Equal("Debug Detail", daprDebugInfoDetail.Detail); + var entry = Assert.Single(daprDebugInfoDetail.StackEntries); + Assert.Equal("Stack Entry", entry); + } + + [Fact] + public void DaprExtendedErrorInfo_ThrowsPreconditionFailureRpcException_ShouldGetSingleDaprPreconditionFailureDetail() + { + // Arrange + DaprExtendedErrorInfo result = null; + var metadataEntry = new Google.Rpc.Status() + { + Code = statusCode, + Message = statusMessage, + }; + + Google.Rpc.PreconditionFailure failure = new(); + + var violationDesc = "Violation Description"; + var violationSubject = "Violation Subject"; + var violationType = "Violation Type"; + + failure.Violations.Add(new Google.Rpc.PreconditionFailure.Types.Violation() + { + Description = violationDesc, + Subject = violationSubject, + Type = violationType + }); + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.PreconditionFailure", Value = failure.ToByteString() }); + + Metadata trailers = new() + { + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } + }; + + var rpcEx = new RpcException(status: new Grpc.Core.Status(Grpc.Core.StatusCode.FailedPrecondition, "PrecondtionFailure"), trailers: trailers); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.NotNull(result); + + var detail = Assert.Single(result.Details); + Assert.Equal(DaprExtendedErrorType.PreconditionFailure, detail.ErrorType); + var preconditionFailureDetail = Assert.IsType(detail); + + var violation = Assert.Single(preconditionFailureDetail.Violations); + + Assert.Equal(violation.Description, violationDesc); + Assert.Equal(violation.Subject, violationSubject); + Assert.Equal(violation.Type, violationType); + } + + [Fact] + public void DaprExtendedErrorInfo_ThrowsHelpRpcException_ShouldGetSingleDaprHelpDetail() + { + // Arrange + DaprExtendedErrorInfo result = null; + var metadataEntry = new Google.Rpc.Status() + { + Code = statusCode, + Message = statusMessage, + }; + + Help help = new(); + + var helpDesc = "Help Description"; + + var helpUrl = "help-link.com"; + + help.Links.Add(new Help.Types.Link() + { + Description = helpDesc, + Url = helpUrl, + }); + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.Help", Value = help.ToByteString() }); + + Metadata trailers = new() + { + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } + }; + + var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "Help"), trailers: trailers); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.NotNull(result); + var detail = Assert.Single(result.Details); + + Assert.IsAssignableFrom(detail); + Assert.Equal(DaprExtendedErrorType.Help, detail.ErrorType); + + var helpDetail = Assert.IsType(detail); + + var link = Assert.Single(helpDetail.Links); + + Assert.Equal(helpDesc, link.Description); + Assert.Equal(helpUrl, link.Url); + } + + [Fact] + public void DaprExtendedErrorInfo_ThrowsResourceInfoRpcException_ShouldGetSingleDaprResourceInfoDetail() + { + // Arrange + DaprExtendedErrorInfo result = null; + var metadataEntry = new Google.Rpc.Status() + { + Code = statusCode, + Message = statusMessage, + }; + + var resourceInfoDesc = "Description"; + var resourceInfoOwner = "Owner"; + var resourceInfoName = "Name"; + var resourceInfoType = "Type"; + + ResourceInfo resourceInfo = new() + { + Description = resourceInfoDesc, + Owner = resourceInfoOwner, + ResourceName = resourceInfoName, + ResourceType = resourceInfoType, + }; + + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.ResourceInfo", Value = resourceInfo.ToByteString() }); + + Metadata trailers = new() + { + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } + }; + + var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "ResourceInfo"), trailers: trailers); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.NotNull(result); + + var detail = Assert.Single(result.Details); + Assert.IsAssignableFrom(detail); + Assert.Equal(DaprExtendedErrorType.ResourceInfo, detail.ErrorType); + + var daprResourceInfo = Assert.IsType(detail); + + Assert.Equal(resourceInfoDesc, daprResourceInfo.Description); + Assert.Equal(resourceInfoName, daprResourceInfo.ResourceName); + Assert.Equal(resourceInfoType, daprResourceInfo.ResourceType); + Assert.Equal(resourceInfoOwner, daprResourceInfo.Owner); + } + + [Fact] + public void DaprExtendedErrorInfo_ThrowsQuotaFailureRpcException_ShouldGetSingleDaprQuotaFailureDetail() + { + // Arrange + DaprExtendedErrorInfo result = null; + var metadataEntry = new Google.Rpc.Status() + { + Code = statusCode, + Message = statusMessage, + }; + + var quotaFailureDesc = "Description"; + var quotaFailureSubject = "Subject"; + + QuotaFailure quotaFailure = new(); + + quotaFailure.Violations.Add(new QuotaFailure.Types.Violation() + { + Description = quotaFailureDesc, + Subject = quotaFailureSubject, + }); + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.QuotaFailure", Value = quotaFailure.ToByteString() }); + + Metadata trailers = new() + { + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } + }; + + var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "QuotaFailure"), trailers: trailers); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.NotNull(result); + + var detail = Assert.Single(result.Details); + + Assert.IsAssignableFrom(detail); + Assert.Equal(DaprExtendedErrorType.QuotaFailure, detail.ErrorType); + + var quotaFailureDetail = Assert.IsType(detail); + + var violation = Assert.Single(quotaFailureDetail.Violations); + Assert.Equal(quotaFailureDesc, violation.Description); + Assert.Equal(quotaFailureSubject, violation.Subject); + } + + [Fact] + public void DaprExtendedErrorInfo_ThrowsErrorInfoRpcException_ShouldGetSingleDaprErrorInfoDetail() + { + // Arrange + DaprExtendedErrorInfo result = null; + var metadataEntry = new Google.Rpc.Status() + { + Code = statusCode, + Message = statusMessage, + }; + + var errorInfoDomain = "Domain"; + var errorInfoReason = "Reason"; + var errorInfoMetadataKey = "Key"; + var errorInfoMetadataValue = "Value"; + var errorInfoMetadata = new Dictionary() + { + { errorInfoMetadataKey, errorInfoMetadataValue } + }; + + Google.Rpc.ErrorInfo errorInfo = new() + { + Domain = errorInfoDomain, + Reason = errorInfoReason, + }; + + errorInfo.Metadata.Add(errorInfoMetadata); + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.ErrorInfo", Value = errorInfo.ToByteString() }); + + Metadata trailers = new() + { + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } + }; + + var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "ErrorInfo"), trailers: trailers); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.NotNull(result); + + var detail = Assert.Single(result.Details); + + Assert.Equal(DaprExtendedErrorType.ErrorInfo, detail.ErrorType); + + var errorInfoDetail = Assert.IsType(detail); + + Assert.Equal(errorInfoDomain, errorInfoDetail.Domain); + Assert.Equal(errorInfoReason, errorInfoDetail.Reason); + + var metadata = Assert.Single(errorInfoDetail.Metadata); + Assert.Equal(errorInfoMetadataKey, metadata.Key); + Assert.Equal(errorInfoMetadataValue, metadata.Value); + } + + [Fact] + public void DaprExtendedErrorInfo_ThrowsRequestInfoRpcException_ShouldGetSingleDaprRequestInfoDetail() + { + // Arrange + DaprExtendedErrorInfo result = null; + var metadataEntry = new Google.Rpc.Status() + { + Code = statusCode, + Message = statusMessage, + }; + + var requestInfoId = "RequestId"; + var requestInfoServingData = "Serving Data"; + + RequestInfo requestInfo = new() + { + RequestId = requestInfoId, + ServingData = requestInfoServingData, + }; + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.RequestInfo", Value = requestInfo.ToByteString() }); + + Metadata trailers = new() + { + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } + }; + + var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "RequestInfo"), trailers: trailers); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.NotNull(result); + var detail = Assert.Single(result.Details); + + Assert.Equal(DaprExtendedErrorType.RequestInfo, detail.ErrorType); + var requestInfoDetail = Assert.IsType(detail); + + Assert.Equal(requestInfoId, requestInfoDetail.RequestId); + Assert.Equal(requestInfoServingData, requestInfoDetail.ServingData); + } + + [Fact] + public void DaprExtendedErrorInfo_ThrowsRequestInfoRpcException_ShouldGetMultipleDetails() + { + // Arrange + DaprExtendedErrorInfo result = null; + var metadataEntry = new Google.Rpc.Status() + { + Code = statusCode, + Message = statusMessage, + }; + + List expectedDetailTypes = new() + { + typeof(DaprResourceInfoDetail), + typeof(DaprRequestInfoDetail), + }; + + RequestInfo requestInfo = new(); + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.RequestInfo", Value = requestInfo.ToByteString() }); + + ResourceInfo resourceInfo = new(); + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.ResourceInfo", Value = resourceInfo.ToByteString() }); + + Metadata trailers = new() + { + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } + }; + + var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "RequestInfo"), trailers: trailers); + + // Act, Assert + try + { + ThrowsRpcBasedDaprException(rpcEx); + } + + catch (DaprException daprEx) + { + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.Collection(result.Details, + detail => Assert.Contains(detail.GetType(), expectedDetailTypes), + detail => Assert.Contains(detail.GetType(), expectedDetailTypes) + ); + } + + private static void ThrowsRpcBasedDaprException(RpcException ex) => throw new DaprException("A Dapr exception", ex); + } +}