From 21acec237104a20cd9761a95d0a1fa5fa06ed731 Mon Sep 17 00:00:00 2001 From: jev Date: Mon, 23 Dec 2024 23:45:29 +0000 Subject: [PATCH 01/10] First pass at supporting richer error model in Dapr .NET SDK Signed-off-by: jev-e Signed-off-by: jev --- src/Dapr.Common/Dapr.Common.csproj | 1 + src/Dapr.Common/DaprExceptionExtensions.cs | 315 +++++++++++++++++++++ 2 files changed, 316 insertions(+) create mode 100644 src/Dapr.Common/DaprExceptionExtensions.cs 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/DaprExceptionExtensions.cs b/src/Dapr.Common/DaprExceptionExtensions.cs new file mode 100644 index 000000000..dae6f7258 --- /dev/null +++ b/src/Dapr.Common/DaprExceptionExtensions.cs @@ -0,0 +1,315 @@ +using System.Diagnostics.CodeAnalysis; +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; + +namespace Dapr.Common.Exceptions +{ + /// + /// ExtendedErrorInfoTypes + /// + public enum DaprExtendedErrorType + { + /// + /// Unrecognized Extended Error Type + /// + Unrecognized, + + /// + /// Retry Info + /// + RetryInfo, + + /// + /// DebugInfo + /// + DebugInfo, + + /// + /// QuotaFailure + /// + QuotaFailure, + + /// + /// PreconditionFailure + /// + PreconditionFailure, + + /// + /// RequestInfo + /// + RequestInfo, + + /// + /// LocalizedMessage + /// + LocalizedMessage, + + /// + /// Bad request. + /// + BadRequest, + + /// + /// Info relating to the exception. + /// + ErrorInfo, + + /// + /// Help URL + /// + Help, + + /// + /// ResourceInfo + /// + ResourceInfo + + } + + /// + /// Base class of the Dapr extended error detail. + /// + public abstract record DaprExtendedErrorDetail(DaprExtendedErrorType ErrorType); + + /// + /// An unrecognized detail. + /// + /// Type Url + public sealed record DaprUnrecognizedDetail(string TypeUrl) : DaprExtendedErrorDetail(DaprExtendedErrorType.Unrecognized); + + /// + /// A Debug Info detail. + /// + /// Stack Entries. + /// Detail + public sealed record DaprDebugInfoDetail(string[] StackEntries, string Detail) : DaprExtendedErrorDetail(DaprExtendedErrorType.DebugInfo); + + /// + /// Precondition Violation + /// + /// PreconditionType + /// + /// + public sealed record DaprPreconditionFailureViolation(string Type, string Subject, string Description); + + /// + /// Precondition Failure + /// + public sealed record DaprPreconditionFailureDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.PreconditionFailure) + { + /// + /// Collection of violations + /// + public DaprPreconditionFailureViolation[] Violations { get; init; } = Array.Empty(); + } + + /// + /// Retry info + /// + /// seconds + /// nanos + public sealed record DaprRetryInfoDetail(long Seconds, int Nanos) : DaprExtendedErrorDetail(DaprExtendedErrorType.RetryInfo); + + /// + /// Quota Violation + /// + /// + /// + public sealed record DaprQuotaFailureViolation(string Subject, string Description); + + /// + /// Quota Failure + /// + public sealed record DaprQuotaFailureDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.QuotaFailure) + { + /// + /// Collection of violations + /// + public DaprQuotaFailureViolation[] Violations { get; init; } = Array.Empty(); + } + + /// + /// Bad Request Field Violation + /// + /// + /// + public sealed record DaprBadRequestDetailFieldViolation(string Field, string Description); + + /// + /// Dapr bad request details + /// + public sealed record DaprBadRequestDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.BadRequest) + { + /// + /// Field Violations. + /// + public DaprBadRequestDetailFieldViolation[] FieldViolations { get; init; } = Array.Empty(); + } + + /// + /// RequestInfo + /// + /// RequestId. + /// ServingData. + public sealed record DaprRequestInfoDetail(string RequestId, string ServingData) : DaprExtendedErrorDetail(DaprExtendedErrorType.RequestInfo); + + /// + /// Localized Message. + /// + /// Locale. + /// Message. + public sealed record DaprLocalizedMessageDetail(string Locale, string Message) : DaprExtendedErrorDetail(DaprExtendedErrorType.LocalizedMessage); + + + /// + /// Link + /// + /// + /// + public sealed record DaprHelpDetailLink(string Url, string Description); + + /// + /// Dapr help details + /// + public sealed record DaprHelpDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.Help) + { + /// + /// Links + /// + public DaprHelpDetailLink[] Links { get; init; } = Array.Empty(); + } + + + /// + /// Dapr resource info details + /// + public sealed record DaprResourceInfoDetail(string ResourceType, string ResourceName, string Owner, string Description) : DaprExtendedErrorDetail(DaprExtendedErrorType.ResourceInfo); + + /// + /// Dapr resource info details + /// + /// Reason + /// Domain + public sealed record DaprErrorInfoDetail(string Reason, string Domain) : DaprExtendedErrorDetail(DaprExtendedErrorType.ErrorInfo); + + /// + /// Extended error detail + /// + /// + /// + public sealed record DaprExtendedErrorInfo(int Code, string Message) + { + /// + /// + /// + public DaprExtendedErrorDetail[] Details { get; init; } = Array.Empty(); + } + + /// + /// Provides help extension methods for + /// + public static class DaprExceptionExtensions + { + private static string GrpcDetails = "grpc-status-details-bin"; + + private const string DaprErrorTypeUrl = "type.googleapis.com/"; + + private const string ErrorInfo = $"{DaprErrorTypeUrl}Google.rpc.ErrorInfo"; + private const string RetryInfo = $"{DaprErrorTypeUrl}Google.rpc.RetryInfo"; + private const string DebugInfo = $"{DaprErrorTypeUrl}Google.rpc.DebugInfo"; + private const string QuotaFailure = $"{DaprErrorTypeUrl}Google.rpc.QuotaFailure"; + private const string PreconditionFailure = $"{DaprErrorTypeUrl}Google.rpc.PreconditionFailure"; + private const string BadRequest = $"{DaprErrorTypeUrl}Google.rpc.BadRequest"; + private const string RequestInfo = $"{DaprErrorTypeUrl}Google.rpc.RequestInfo"; + private const string ResourceInfo = $"{DaprErrorTypeUrl}Google.rpc.ResourceInfo"; + private const string Help = $"{DaprErrorTypeUrl}Google.rpc.Help"; + private const string LocalizedMessage = $"{DaprErrorTypeUrl}Google.rpc.LocalizedMessage"; + + /// + /// Attempt to retrieve from + /// + /// + 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(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 => GetDaprErrorDetailFromType(detail)).ToArray(), + }; + + return true; + } + + private static DaprExtendedErrorDetail GetDaprErrorDetailFromType(Any detail) + { + return detail.TypeUrl switch + { + ErrorInfo => ToDaprErrorInfoDetail(Google.Rpc.ErrorInfo.Parser.ParseFrom(detail.Value)), + RetryInfo => ToDaprRetryInfoDetail(Google.Rpc.RetryInfo.Parser.ParseFrom(detail.Value)), + DebugInfo => ToDaprDebugInfoDetail(Google.Rpc.DebugInfo.Parser.ParseFrom(detail.Value)), + QuotaFailure => ToDaprQuotaFailureDetail(Google.Rpc.QuotaFailure.Parser.ParseFrom(detail.Value)), + PreconditionFailure => ToDaprPreconditionFailureDetail(Google.Rpc.PreconditionFailure.Parser.ParseFrom(detail.Value)), + BadRequest => ToDaprBadRequestDetail(Google.Rpc.BadRequest.Parser.ParseFrom(detail.Value)), + RequestInfo => ToDaprRequestInfoDetail(Google.Rpc.RequestInfo.Parser.ParseFrom(detail.Value)), + ResourceInfo => ToDaprResourceInfoDetail(Google.Rpc.ResourceInfo.Parser.ParseFrom(detail.Value)), + Help => ToDaprHelpDetail(Google.Rpc.Help.Parser.ParseFrom(detail.Value)), + LocalizedMessage => ToDaprLocalizedMessageDetail(Google.Rpc.LocalizedMessage.Parser.ParseFrom(detail.Value)), + _ => new DaprUnrecognizedDetail(detail.TypeUrl), + }; + } + + private static DaprRetryInfoDetail ToDaprRetryInfoDetail(Google.Rpc.RetryInfo retryInfo) => + new(Seconds: retryInfo.RetryDelay.Seconds, Nanos: retryInfo.RetryDelay.Nanos); + + private static DaprLocalizedMessageDetail ToDaprLocalizedMessageDetail(Google.Rpc.LocalizedMessage localizedMessage) => + new(Locale: localizedMessage.Locale, Message: localizedMessage.Message); + + private static DaprDebugInfoDetail ToDaprDebugInfoDetail(Google.Rpc.DebugInfo debugInfo) => + new(StackEntries: debugInfo.StackEntries.ToArray(), Detail: debugInfo.Detail); + + private static DaprQuotaFailureDetail ToDaprQuotaFailureDetail(Google.Rpc.QuotaFailure quotaFailure) => + new() + { + Violations = quotaFailure.Violations.Select(violation => new DaprQuotaFailureViolation(Subject: violation.Subject, Description: violation.Description)).ToArray(), + }; + + private static DaprPreconditionFailureDetail ToDaprPreconditionFailureDetail(Google.Rpc.PreconditionFailure preconditionFailure) => + new() { Violations = preconditionFailure.Violations.Select(violation => new DaprPreconditionFailureViolation(Type: violation.Type, Subject: violation.Subject, Description: violation.Description)).ToArray() }; + + private static DaprRequestInfoDetail ToDaprRequestInfoDetail(Google.Rpc.RequestInfo requestInfo) => + new(RequestId: requestInfo.RequestId, ServingData: requestInfo.ServingData); + + private static DaprResourceInfoDetail ToDaprResourceInfoDetail(Google.Rpc.ResourceInfo resourceInfo) => + new(ResourceType: resourceInfo.ResourceType, ResourceName: resourceInfo.ResourceName, Owner: resourceInfo.Owner, Description: resourceInfo.Description); + + private static DaprBadRequestDetail ToDaprBadRequestDetail(Google.Rpc.BadRequest badRequest) => + new() + { + FieldViolations = badRequest.FieldViolations.Select(fieldViolation => + new DaprBadRequestDetailFieldViolation(fieldViolation.Field, fieldViolation.Description)).ToArray() + }; + + private static DaprErrorInfoDetail ToDaprErrorInfoDetail(Google.Rpc.ErrorInfo errorInfo) => + new(Reason: errorInfo.Reason, Domain: errorInfo.Domain); + + private static DaprHelpDetail ToDaprHelpDetail(Google.Rpc.Help help) => + new() + { + Links = help.Links.Select(link => new DaprHelpDetailLink(link.Url, link.Description)).ToArray() + }; + } +} From 4d21f9adc2022e26f6c1eb961b397d82167c6f20 Mon Sep 17 00:00:00 2001 From: jev Date: Tue, 24 Dec 2024 15:53:48 +0000 Subject: [PATCH 02/10] Add ExtendedErrorDetailFactory, move to seperate files / new folder, add test file. Signed-off-by: jev --- src/Dapr.Common/DaprExceptionExtensions.cs | 315 ------------------ .../Exceptions/DaprExceptionExtensions.cs | 42 +++ .../Exceptions/DaprExtendedErrorDetail.cs | 139 ++++++++ .../Exceptions/DaprExtendedErrorInfo.cs | 15 + .../Exceptions/DaprExtendedErrorType.cs | 63 ++++ .../Exceptions/ExtendedErrorDetailFactory.cs | 116 +++++++ test/Dapr.Common.Test/DaprErrorsTest.cs | 39 +++ 7 files changed, 414 insertions(+), 315 deletions(-) delete mode 100644 src/Dapr.Common/DaprExceptionExtensions.cs create mode 100644 src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs create mode 100644 src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs create mode 100644 src/Dapr.Common/Exceptions/DaprExtendedErrorInfo.cs create mode 100644 src/Dapr.Common/Exceptions/DaprExtendedErrorType.cs create mode 100644 src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs create mode 100644 test/Dapr.Common.Test/DaprErrorsTest.cs diff --git a/src/Dapr.Common/DaprExceptionExtensions.cs b/src/Dapr.Common/DaprExceptionExtensions.cs deleted file mode 100644 index dae6f7258..000000000 --- a/src/Dapr.Common/DaprExceptionExtensions.cs +++ /dev/null @@ -1,315 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Google.Protobuf.WellKnownTypes; -using Grpc.Core; - -namespace Dapr.Common.Exceptions -{ - /// - /// ExtendedErrorInfoTypes - /// - public enum DaprExtendedErrorType - { - /// - /// Unrecognized Extended Error Type - /// - Unrecognized, - - /// - /// Retry Info - /// - RetryInfo, - - /// - /// DebugInfo - /// - DebugInfo, - - /// - /// QuotaFailure - /// - QuotaFailure, - - /// - /// PreconditionFailure - /// - PreconditionFailure, - - /// - /// RequestInfo - /// - RequestInfo, - - /// - /// LocalizedMessage - /// - LocalizedMessage, - - /// - /// Bad request. - /// - BadRequest, - - /// - /// Info relating to the exception. - /// - ErrorInfo, - - /// - /// Help URL - /// - Help, - - /// - /// ResourceInfo - /// - ResourceInfo - - } - - /// - /// Base class of the Dapr extended error detail. - /// - public abstract record DaprExtendedErrorDetail(DaprExtendedErrorType ErrorType); - - /// - /// An unrecognized detail. - /// - /// Type Url - public sealed record DaprUnrecognizedDetail(string TypeUrl) : DaprExtendedErrorDetail(DaprExtendedErrorType.Unrecognized); - - /// - /// A Debug Info detail. - /// - /// Stack Entries. - /// Detail - public sealed record DaprDebugInfoDetail(string[] StackEntries, string Detail) : DaprExtendedErrorDetail(DaprExtendedErrorType.DebugInfo); - - /// - /// Precondition Violation - /// - /// PreconditionType - /// - /// - public sealed record DaprPreconditionFailureViolation(string Type, string Subject, string Description); - - /// - /// Precondition Failure - /// - public sealed record DaprPreconditionFailureDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.PreconditionFailure) - { - /// - /// Collection of violations - /// - public DaprPreconditionFailureViolation[] Violations { get; init; } = Array.Empty(); - } - - /// - /// Retry info - /// - /// seconds - /// nanos - public sealed record DaprRetryInfoDetail(long Seconds, int Nanos) : DaprExtendedErrorDetail(DaprExtendedErrorType.RetryInfo); - - /// - /// Quota Violation - /// - /// - /// - public sealed record DaprQuotaFailureViolation(string Subject, string Description); - - /// - /// Quota Failure - /// - public sealed record DaprQuotaFailureDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.QuotaFailure) - { - /// - /// Collection of violations - /// - public DaprQuotaFailureViolation[] Violations { get; init; } = Array.Empty(); - } - - /// - /// Bad Request Field Violation - /// - /// - /// - public sealed record DaprBadRequestDetailFieldViolation(string Field, string Description); - - /// - /// Dapr bad request details - /// - public sealed record DaprBadRequestDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.BadRequest) - { - /// - /// Field Violations. - /// - public DaprBadRequestDetailFieldViolation[] FieldViolations { get; init; } = Array.Empty(); - } - - /// - /// RequestInfo - /// - /// RequestId. - /// ServingData. - public sealed record DaprRequestInfoDetail(string RequestId, string ServingData) : DaprExtendedErrorDetail(DaprExtendedErrorType.RequestInfo); - - /// - /// Localized Message. - /// - /// Locale. - /// Message. - public sealed record DaprLocalizedMessageDetail(string Locale, string Message) : DaprExtendedErrorDetail(DaprExtendedErrorType.LocalizedMessage); - - - /// - /// Link - /// - /// - /// - public sealed record DaprHelpDetailLink(string Url, string Description); - - /// - /// Dapr help details - /// - public sealed record DaprHelpDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.Help) - { - /// - /// Links - /// - public DaprHelpDetailLink[] Links { get; init; } = Array.Empty(); - } - - - /// - /// Dapr resource info details - /// - public sealed record DaprResourceInfoDetail(string ResourceType, string ResourceName, string Owner, string Description) : DaprExtendedErrorDetail(DaprExtendedErrorType.ResourceInfo); - - /// - /// Dapr resource info details - /// - /// Reason - /// Domain - public sealed record DaprErrorInfoDetail(string Reason, string Domain) : DaprExtendedErrorDetail(DaprExtendedErrorType.ErrorInfo); - - /// - /// Extended error detail - /// - /// - /// - public sealed record DaprExtendedErrorInfo(int Code, string Message) - { - /// - /// - /// - public DaprExtendedErrorDetail[] Details { get; init; } = Array.Empty(); - } - - /// - /// Provides help extension methods for - /// - public static class DaprExceptionExtensions - { - private static string GrpcDetails = "grpc-status-details-bin"; - - private const string DaprErrorTypeUrl = "type.googleapis.com/"; - - private const string ErrorInfo = $"{DaprErrorTypeUrl}Google.rpc.ErrorInfo"; - private const string RetryInfo = $"{DaprErrorTypeUrl}Google.rpc.RetryInfo"; - private const string DebugInfo = $"{DaprErrorTypeUrl}Google.rpc.DebugInfo"; - private const string QuotaFailure = $"{DaprErrorTypeUrl}Google.rpc.QuotaFailure"; - private const string PreconditionFailure = $"{DaprErrorTypeUrl}Google.rpc.PreconditionFailure"; - private const string BadRequest = $"{DaprErrorTypeUrl}Google.rpc.BadRequest"; - private const string RequestInfo = $"{DaprErrorTypeUrl}Google.rpc.RequestInfo"; - private const string ResourceInfo = $"{DaprErrorTypeUrl}Google.rpc.ResourceInfo"; - private const string Help = $"{DaprErrorTypeUrl}Google.rpc.Help"; - private const string LocalizedMessage = $"{DaprErrorTypeUrl}Google.rpc.LocalizedMessage"; - - /// - /// Attempt to retrieve from - /// - /// - 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(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 => GetDaprErrorDetailFromType(detail)).ToArray(), - }; - - return true; - } - - private static DaprExtendedErrorDetail GetDaprErrorDetailFromType(Any detail) - { - return detail.TypeUrl switch - { - ErrorInfo => ToDaprErrorInfoDetail(Google.Rpc.ErrorInfo.Parser.ParseFrom(detail.Value)), - RetryInfo => ToDaprRetryInfoDetail(Google.Rpc.RetryInfo.Parser.ParseFrom(detail.Value)), - DebugInfo => ToDaprDebugInfoDetail(Google.Rpc.DebugInfo.Parser.ParseFrom(detail.Value)), - QuotaFailure => ToDaprQuotaFailureDetail(Google.Rpc.QuotaFailure.Parser.ParseFrom(detail.Value)), - PreconditionFailure => ToDaprPreconditionFailureDetail(Google.Rpc.PreconditionFailure.Parser.ParseFrom(detail.Value)), - BadRequest => ToDaprBadRequestDetail(Google.Rpc.BadRequest.Parser.ParseFrom(detail.Value)), - RequestInfo => ToDaprRequestInfoDetail(Google.Rpc.RequestInfo.Parser.ParseFrom(detail.Value)), - ResourceInfo => ToDaprResourceInfoDetail(Google.Rpc.ResourceInfo.Parser.ParseFrom(detail.Value)), - Help => ToDaprHelpDetail(Google.Rpc.Help.Parser.ParseFrom(detail.Value)), - LocalizedMessage => ToDaprLocalizedMessageDetail(Google.Rpc.LocalizedMessage.Parser.ParseFrom(detail.Value)), - _ => new DaprUnrecognizedDetail(detail.TypeUrl), - }; - } - - private static DaprRetryInfoDetail ToDaprRetryInfoDetail(Google.Rpc.RetryInfo retryInfo) => - new(Seconds: retryInfo.RetryDelay.Seconds, Nanos: retryInfo.RetryDelay.Nanos); - - private static DaprLocalizedMessageDetail ToDaprLocalizedMessageDetail(Google.Rpc.LocalizedMessage localizedMessage) => - new(Locale: localizedMessage.Locale, Message: localizedMessage.Message); - - private static DaprDebugInfoDetail ToDaprDebugInfoDetail(Google.Rpc.DebugInfo debugInfo) => - new(StackEntries: debugInfo.StackEntries.ToArray(), Detail: debugInfo.Detail); - - private static DaprQuotaFailureDetail ToDaprQuotaFailureDetail(Google.Rpc.QuotaFailure quotaFailure) => - new() - { - Violations = quotaFailure.Violations.Select(violation => new DaprQuotaFailureViolation(Subject: violation.Subject, Description: violation.Description)).ToArray(), - }; - - private static DaprPreconditionFailureDetail ToDaprPreconditionFailureDetail(Google.Rpc.PreconditionFailure preconditionFailure) => - new() { Violations = preconditionFailure.Violations.Select(violation => new DaprPreconditionFailureViolation(Type: violation.Type, Subject: violation.Subject, Description: violation.Description)).ToArray() }; - - private static DaprRequestInfoDetail ToDaprRequestInfoDetail(Google.Rpc.RequestInfo requestInfo) => - new(RequestId: requestInfo.RequestId, ServingData: requestInfo.ServingData); - - private static DaprResourceInfoDetail ToDaprResourceInfoDetail(Google.Rpc.ResourceInfo resourceInfo) => - new(ResourceType: resourceInfo.ResourceType, ResourceName: resourceInfo.ResourceName, Owner: resourceInfo.Owner, Description: resourceInfo.Description); - - private static DaprBadRequestDetail ToDaprBadRequestDetail(Google.Rpc.BadRequest badRequest) => - new() - { - FieldViolations = badRequest.FieldViolations.Select(fieldViolation => - new DaprBadRequestDetailFieldViolation(fieldViolation.Field, fieldViolation.Description)).ToArray() - }; - - private static DaprErrorInfoDetail ToDaprErrorInfoDetail(Google.Rpc.ErrorInfo errorInfo) => - new(Reason: errorInfo.Reason, Domain: errorInfo.Domain); - - private static DaprHelpDetail ToDaprHelpDetail(Google.Rpc.Help help) => - new() - { - Links = help.Links.Select(link => new DaprHelpDetailLink(link.Url, link.Description)).ToArray() - }; - } -} diff --git a/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs b/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs new file mode 100644 index 000000000..7d10b195b --- /dev/null +++ b/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs @@ -0,0 +1,42 @@ +using System.Diagnostics.CodeAnalysis; +using Grpc.Core; + +namespace Dapr.Common.Exceptions +{ + /// + /// Provides help extension methods for + /// + public static class DaprExceptionExtensions + { + private static string GrpcDetails = "grpc-status-details-bin"; + + /// + /// Attempt to retrieve from + /// + /// + 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(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/DaprExtendedErrorDetail.cs b/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs new file mode 100644 index 000000000..ec2088edb --- /dev/null +++ b/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs @@ -0,0 +1,139 @@ +namespace Dapr.Common.Exceptions +{ + /// + /// Base class of the Dapr extended error detail. + /// + public abstract record DaprExtendedErrorDetail(DaprExtendedErrorType ErrorType); + + /// + /// An unrecognized detail. + /// + /// Type Url + public sealed record DaprUnrecognizedDetail(string TypeUrl) : DaprExtendedErrorDetail(DaprExtendedErrorType.Unrecognized); + + /// + /// A Debug Info detail. + /// + /// Stack Entries. + /// Detail + public sealed record DaprDebugInfoDetail(string[] StackEntries, string Detail) : DaprExtendedErrorDetail(DaprExtendedErrorType.DebugInfo); + + /// + /// A Precondition Violation. + /// + /// PreconditionType. + /// Subject. + /// A Description. + public sealed record DaprPreconditionFailureViolation(string Type, string Subject, string Description); + + /// + /// A Precondition Failure detail. + /// + public sealed record DaprPreconditionFailureDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.PreconditionFailure) + { + /// + /// Collection of . + /// + public DaprPreconditionFailureViolation[] Violations { get; init; } = Array.Empty(); + } + + /// + /// Retry information. + /// + /// Second offset. + /// Nano offset. + public sealed record DaprRetryDelay(long Seconds, int Nanos); + + /// + /// A Retry Info detail. + /// + public sealed record DaprRetryInfoDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.RetryInfo) + { + /// + /// Provides information of amount of time until retry should be attempted. + /// + public DaprRetryDelay Delay = new(Seconds: 1, Nanos: default); + } + + /// + /// A Quota Violation. + /// + /// The Subject. + /// A Description. + public sealed record DaprQuotaFailureViolation(string Subject, string Description); + + /// + /// A Quota Failure detail. + /// + public sealed record DaprQuotaFailureDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.QuotaFailure) + { + /// + /// Collection of . + /// + public DaprQuotaFailureViolation[] Violations { get; init; } = Array.Empty(); + } + + /// + /// Bad Request Field Violation + /// + /// + /// + public sealed record DaprBadRequestDetailFieldViolation(string Field, string Description); + + /// + /// Dapr bad request details + /// + public sealed record DaprBadRequestDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.BadRequest) + { + /// + /// Collection of . + /// + public DaprBadRequestDetailFieldViolation[] FieldViolations { get; init; } = Array.Empty(); + } + + /// + /// Request Info. + /// + /// A RequestId. + /// ServingData. + public sealed record DaprRequestInfoDetail(string RequestId, string ServingData) : DaprExtendedErrorDetail(DaprExtendedErrorType.RequestInfo); + + /// + /// Localized Message. + /// + /// Locale. + /// Message. + public sealed record DaprLocalizedMessageDetail(string Locale, string Message) : DaprExtendedErrorDetail(DaprExtendedErrorType.LocalizedMessage); + + + /// + /// A link to help resources. + /// + /// Url to help details. + /// A description. + public sealed record DaprHelpDetailLink(string Url, string Description); + + /// + /// Dapr help details + /// + public sealed record DaprHelpDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.Help) + { + /// + /// Collection of . + /// + public DaprHelpDetailLink[] Links { get; init; } = Array.Empty(); + } + + + /// + /// Dapr resource info details. + /// + public sealed record DaprResourceInfoDetail(string ResourceType, string ResourceName, string Owner, string Description) : DaprExtendedErrorDetail(DaprExtendedErrorType.ResourceInfo); + + /// + /// Dapr error info details. + /// + /// Reason + /// Domain + public sealed record DaprErrorInfoDetail(string Reason, string Domain) : 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..2f3fb9fc8 --- /dev/null +++ b/src/Dapr.Common/Exceptions/DaprExtendedErrorInfo.cs @@ -0,0 +1,15 @@ +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..33300e4b8 --- /dev/null +++ b/src/Dapr.Common/Exceptions/DaprExtendedErrorType.cs @@ -0,0 +1,63 @@ +namespace Dapr.Common.Exceptions +{ + /// + /// Extended Error Detail Types + /// + public enum DaprExtendedErrorType + { + /// + /// Unrecognized Extended Error Type. + /// + Unrecognized, + + /// + /// Retry Info Detail Type. + /// + RetryInfo, + + /// + /// Debug Info Detail Type. + /// + DebugInfo, + + /// + /// Quote Failure Detail Type. + /// + QuotaFailure, + + /// + /// Precondition Failure Detail Type. + /// + PreconditionFailure, + + /// + /// Request Info Detail Type. + /// + RequestInfo, + + /// + /// Localized Message Detail Type. + /// + LocalizedMessage, + + /// + /// Bad Request Detail Type. + /// + BadRequest, + + /// + /// Error Info Detail Type. + /// + ErrorInfo, + + /// + /// Help Detail Type. + /// + Help, + + /// + /// Resource Info Detail Type. + /// + ResourceInfo + } +} diff --git a/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs b/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs new file mode 100644 index 000000000..ff87a9842 --- /dev/null +++ b/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs @@ -0,0 +1,116 @@ +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; +using Google.Rpc; + +namespace Dapr.Common.Exceptions +{ + /// + /// factory class. + /// + internal static class ExtendedErrorDetailFactory + { + private const string DaprErrorTypeUrl = "type.googleapis.com/"; + + private static Dictionary> extendedErrorTypeMapping = + new() + { + { $"{DaprErrorTypeUrl}Google.rpc.ErrorInfo", ToDaprErrorInfoDetail }, + { $"{DaprErrorTypeUrl}Google.rpc.RetryInfo", ToDaprRetryInfoDetail }, + { $"{DaprErrorTypeUrl}Google.rpc.DebugInfo", ToDaprDebugInfoDetail }, + { $"{DaprErrorTypeUrl}Google.rpc.QuotaFailure", ToDaprQuotaFailureDetail }, + { $"{DaprErrorTypeUrl}Google.rpc.PreconditionFailure", ToDaprPreconditionFailureDetail }, + { $"{DaprErrorTypeUrl}Google.rpc.BadRequest", ToDaprBadRequestDetail }, + { $"{DaprErrorTypeUrl}Google.rpc.RequestInfo", ToDaprRequestInfoDetail }, + { $"{DaprErrorTypeUrl}Google.rpc.ResourceInfo", ToDaprResourceInfoDetail }, + { $"{DaprErrorTypeUrl}Google.rpc.Help", ToDaprHelpDetail }, + { $"{DaprErrorTypeUrl}Google.rpc.LocalizedMessage", ToDaprLocalizedMessageDetail }, + }; + + /// + /// Create a new from an instance of . + /// + /// The to create from. + /// A new instance of + internal static DaprExtendedErrorDetail CreateErrorDetail(Any metadata) + { + if (!extendedErrorTypeMapping.TryGetValue(metadata.TypeUrl, out var create)) + { + return new DaprUnrecognizedDetail(metadata.TypeUrl); + } + + return create.Invoke(metadata.Value); + } + + 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(fieldViolation.Field, fieldViolation.Description)).ToArray() + }; + } + + private static DaprErrorInfoDetail ToDaprErrorInfoDetail(ByteString data) + { + var errorInfo = ErrorInfo.Parser.ParseFrom(data); + return new(Reason: errorInfo.Reason, Domain: errorInfo.Domain); + } + + private static DaprHelpDetail ToDaprHelpDetail(ByteString data) + { + var helpInfo = Help.Parser.ParseFrom(data); + return new() + { + Links = helpInfo.Links.Select(link => new DaprHelpDetailLink(link.Url, link.Description)).ToArray() + }; + } + } +} diff --git a/test/Dapr.Common.Test/DaprErrorsTest.cs b/test/Dapr.Common.Test/DaprErrorsTest.cs new file mode 100644 index 000000000..c15205f8c --- /dev/null +++ b/test/Dapr.Common.Test/DaprErrorsTest.cs @@ -0,0 +1,39 @@ +using Grpc.Core; +using Xunit; + +namespace Dapr.Common.Test +{ + public class DaprErrorsTest + { + [Fact] + public void DaprExtendedErrorInfo_ThrowsRpcException_ShouldGetExtendedErrorInfo() + { + // Arrange + + + // Act + //try + //{ + // ThrowsRpcBasedDaprException(); + //} + + // catch (DaprException daprEx) + // { + // daprEx. + // } + + } + + private static void ThrowsRpcBasedDaprException() + { + try + { + throw new RpcException(new Status(StatusCode.Aborted, "Operation Aborted")); + } + catch (RpcException ex) + { + throw new DaprException("An Error Occured", ex); + } + } + } +} From 8ff95398232ec105e1b32bc74fe6b37019fad5eb Mon Sep 17 00:00:00 2001 From: Jev Date: Tue, 24 Dec 2024 22:59:33 +0000 Subject: [PATCH 03/10] Flesh out + rename tests file, tidy more comments. Signed-off-by: jev --- .../Exceptions/DaprExceptionExtensions.cs | 2 +- .../Exceptions/DaprExtendedErrorDetail.cs | 4 +- .../Exceptions/DaprExtendedErrorType.cs | 2 +- .../Exceptions/ExtendedErrorDetailFactory.cs | 7 +- test/Dapr.Common.Test/DaprErrorsTest.cs | 39 ---------- .../DaprExtendedErrorInfoTest.cs | 78 +++++++++++++++++++ 6 files changed, 86 insertions(+), 46 deletions(-) delete mode 100644 test/Dapr.Common.Test/DaprErrorsTest.cs create mode 100644 test/Dapr.Common.Test/DaprExtendedErrorInfoTest.cs diff --git a/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs b/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs index 7d10b195b..78d1686a2 100644 --- a/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs +++ b/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs @@ -4,7 +4,7 @@ namespace Dapr.Common.Exceptions { /// - /// Provides help extension methods for + /// Provides extension methods for /// public static class DaprExceptionExtensions { diff --git a/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs b/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs index ec2088edb..f8c94e284 100644 --- a/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs +++ b/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs @@ -8,14 +8,14 @@ public abstract record DaprExtendedErrorDetail(DaprExtendedErrorType ErrorType); /// /// An unrecognized detail. /// - /// Type Url + /// Type Url. public sealed record DaprUnrecognizedDetail(string TypeUrl) : DaprExtendedErrorDetail(DaprExtendedErrorType.Unrecognized); /// /// A Debug Info detail. /// /// Stack Entries. - /// Detail + /// Detail. public sealed record DaprDebugInfoDetail(string[] StackEntries, string Detail) : DaprExtendedErrorDetail(DaprExtendedErrorType.DebugInfo); /// diff --git a/src/Dapr.Common/Exceptions/DaprExtendedErrorType.cs b/src/Dapr.Common/Exceptions/DaprExtendedErrorType.cs index 33300e4b8..1356a2439 100644 --- a/src/Dapr.Common/Exceptions/DaprExtendedErrorType.cs +++ b/src/Dapr.Common/Exceptions/DaprExtendedErrorType.cs @@ -1,7 +1,7 @@ namespace Dapr.Common.Exceptions { /// - /// Extended Error Detail Types + /// Extended Error Detail Types. /// public enum DaprExtendedErrorType { diff --git a/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs b/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs index ff87a9842..a60b7f621 100644 --- a/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs +++ b/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs @@ -29,7 +29,7 @@ internal static class ExtendedErrorDetailFactory /// /// Create a new from an instance of . /// - /// The to create from. + /// The message to create the error detail from. /// A new instance of internal static DaprExtendedErrorDetail CreateErrorDetail(Any metadata) { @@ -94,7 +94,8 @@ private static DaprBadRequestDetail ToDaprBadRequestDetail(ByteString data) var badRequest = BadRequest.Parser.ParseFrom(data); return new() { - FieldViolations = badRequest.FieldViolations.Select(fieldViolation => new DaprBadRequestDetailFieldViolation(fieldViolation.Field, fieldViolation.Description)).ToArray() + FieldViolations = badRequest.FieldViolations.Select( + fieldViolation => new DaprBadRequestDetailFieldViolation(Field: fieldViolation.Field, Description: fieldViolation.Description)).ToArray() }; } @@ -109,7 +110,7 @@ private static DaprHelpDetail ToDaprHelpDetail(ByteString data) var helpInfo = Help.Parser.ParseFrom(data); return new() { - Links = helpInfo.Links.Select(link => new DaprHelpDetailLink(link.Url, link.Description)).ToArray() + Links = helpInfo.Links.Select(link => new DaprHelpDetailLink(Url: link.Url, Description: link.Description)).ToArray() }; } } diff --git a/test/Dapr.Common.Test/DaprErrorsTest.cs b/test/Dapr.Common.Test/DaprErrorsTest.cs deleted file mode 100644 index c15205f8c..000000000 --- a/test/Dapr.Common.Test/DaprErrorsTest.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Grpc.Core; -using Xunit; - -namespace Dapr.Common.Test -{ - public class DaprErrorsTest - { - [Fact] - public void DaprExtendedErrorInfo_ThrowsRpcException_ShouldGetExtendedErrorInfo() - { - // Arrange - - - // Act - //try - //{ - // ThrowsRpcBasedDaprException(); - //} - - // catch (DaprException daprEx) - // { - // daprEx. - // } - - } - - private static void ThrowsRpcBasedDaprException() - { - try - { - throw new RpcException(new Status(StatusCode.Aborted, "Operation Aborted")); - } - catch (RpcException ex) - { - throw new DaprException("An Error Occured", ex); - } - } - } -} diff --git a/test/Dapr.Common.Test/DaprExtendedErrorInfoTest.cs b/test/Dapr.Common.Test/DaprExtendedErrorInfoTest.cs new file mode 100644 index 000000000..3f73f93d8 --- /dev/null +++ b/test/Dapr.Common.Test/DaprExtendedErrorInfoTest.cs @@ -0,0 +1,78 @@ +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 string GrpcDetails = "grpc-status-details-bin"; + + public static IEnumerable supportedRicherErrorDetails = new List() + { + new object[] { DaprExtendedErrorType.BadRequest, typeof(DaprBadRequestDetail) }, + new object[] { DaprExtendedErrorType.LocalizedMessage, typeof(DaprLocalizedMessageDetail)}, + new object[] { DaprExtendedErrorType.Help, typeof(DaprHelpDetail )}, + new object[] { DaprExtendedErrorType.PreconditionFailure, typeof(DaprPreconditionFailureDetail )}, + new object[] { DaprExtendedErrorType.QuotaFailure, typeof(DaprQuotaFailureDetail )}, + new object[] { DaprExtendedErrorType.DebugInfo, typeof(DaprDebugInfoDetail )}, + new object[] { DaprExtendedErrorType.ErrorInfo, typeof(DaprErrorInfoDetail )}, + new object[] { DaprExtendedErrorType.ResourceInfo, typeof(DaprResourceInfoDetail )}, + new object[] { DaprExtendedErrorType.RetryInfo, typeof(DaprRetryInfoDetail )}, + new object[] { DaprExtendedErrorType.RequestInfo, typeof(DaprRequestInfoDetail )}, + }; + + + [Fact] + public void DaprExtendedErrorInfo_ThrowsRpcException_ShouldGetExtendedErrorInfo() + { + // Arrange + DaprExtendedErrorInfo result = null; + + // Act + try + { + ThrowsRpcBasedDaprException(); + } + + catch (DaprException daprEx) + { + Assert.True(daprEx.TryGetExtendedErrorInfo(out result)); + } + + Assert.NotNull(result); + Assert.Single(result.Details); + Assert.Collection(result.Details, entry => + { + Assert.IsAssignableFrom(entry); + Assert.IsType(entry); + }); + } + + private static void ThrowsRpcBasedDaprException() + { + var metadataEntry = new Google.Rpc.Status() + { + Code = 1, + Message = "Status Message", + }; + + PreconditionFailure failure = new(); + + metadataEntry.Details.Add(new Any() { TypeUrl = "type.googleapis.com/Google.rpc.PreconditionFailure", Value = failure.ToByteString() }); + + + Metadata trailers = new() + { + { GrpcDetails, metadataEntry.ToByteArray() } + }; + + throw new DaprException("RpcBasedDaprException", new RpcException(status: new Grpc.Core.Status(StatusCode.FailedPrecondition, "PrecondtionFailure"), trailers: trailers)); + } + } +} From 2ffe30ec588b5746f77af9717c680ee2c0b89177 Mon Sep 17 00:00:00 2001 From: Jev Date: Wed, 25 Dec 2024 18:14:31 +0000 Subject: [PATCH 04/10] Add metadata to ErrorInfo details, add tests for each details type, multiple details Signed-off-by: jev jacob@jev.org.uk Signed-off-by: jev --- .../Exceptions/DaprExtendedErrorDetail.cs | 10 +- .../Exceptions/ExtendedErrorDetailFactory.cs | 2 +- .../DaprExtendedErrorInfoTest.cs | 660 +++++++++++++++++- 3 files changed, 642 insertions(+), 30 deletions(-) diff --git a/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs b/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs index f8c94e284..01e34b9ad 100644 --- a/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs +++ b/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs @@ -133,7 +133,9 @@ public sealed record DaprResourceInfoDetail(string ResourceType, string Resource /// /// Dapr error info details. /// - /// Reason - /// Domain - public sealed record DaprErrorInfoDetail(string Reason, string Domain) : DaprExtendedErrorDetail(DaprExtendedErrorType.ErrorInfo); -} + /// Reason. + /// Domain. + /// Metadata. + public sealed record DaprErrorInfoDetail(string Reason, string Domain, IDictionary? Metadata) : DaprExtendedErrorDetail(DaprExtendedErrorType.ErrorInfo); + +} diff --git a/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs b/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs index a60b7f621..c05621bca 100644 --- a/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs +++ b/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs @@ -102,7 +102,7 @@ private static DaprBadRequestDetail ToDaprBadRequestDetail(ByteString data) private static DaprErrorInfoDetail ToDaprErrorInfoDetail(ByteString data) { var errorInfo = ErrorInfo.Parser.ParseFrom(data); - return new(Reason: errorInfo.Reason, Domain: errorInfo.Domain); + return new(Reason: errorInfo.Reason, Domain: errorInfo.Domain, Metadata: errorInfo.Metadata); } private static DaprHelpDetail ToDaprHelpDetail(ByteString data) diff --git a/test/Dapr.Common.Test/DaprExtendedErrorInfoTest.cs b/test/Dapr.Common.Test/DaprExtendedErrorInfoTest.cs index 3f73f93d8..47ac64f39 100644 --- a/test/Dapr.Common.Test/DaprExtendedErrorInfoTest.cs +++ b/test/Dapr.Common.Test/DaprExtendedErrorInfoTest.cs @@ -10,34 +10,124 @@ namespace Dapr.Common.Test { public class DaprExtendedErrorInfoTest { + private static int statusCode = 1; + private static string statusMessage = "Status Message"; private static string GrpcDetails = "grpc-status-details-bin"; - public static IEnumerable supportedRicherErrorDetails = new List() + [Fact] + public void DaprExendedErrorInfo_ThrowsRpcDaprException_ExtendedErrorInfoNotNull() { - new object[] { DaprExtendedErrorType.BadRequest, typeof(DaprBadRequestDetail) }, - new object[] { DaprExtendedErrorType.LocalizedMessage, typeof(DaprLocalizedMessageDetail)}, - new object[] { DaprExtendedErrorType.Help, typeof(DaprHelpDetail )}, - new object[] { DaprExtendedErrorType.PreconditionFailure, typeof(DaprPreconditionFailureDetail )}, - new object[] { DaprExtendedErrorType.QuotaFailure, typeof(DaprQuotaFailureDetail )}, - new object[] { DaprExtendedErrorType.DebugInfo, typeof(DaprDebugInfoDetail )}, - new object[] { DaprExtendedErrorType.ErrorInfo, typeof(DaprErrorInfoDetail )}, - new object[] { DaprExtendedErrorType.ResourceInfo, typeof(DaprResourceInfoDetail )}, - new object[] { DaprExtendedErrorType.RetryInfo, typeof(DaprRetryInfoDetail )}, - new object[] { DaprExtendedErrorType.RequestInfo, typeof(DaprRequestInfoDetail )}, - }; + // 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() + { + { 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 DaprExtendedErrorInfo_ThrowsRpcException_ShouldGetExtendedErrorInfo() + public void DaprExendedErrorInfo_ThrowsNonRpcDaprException_ExtendedErrorInfoIsNull() { // Arrange DaprExtendedErrorInfo result = null; - // Act + // Act, Assert try { - ThrowsRpcBasedDaprException(); + 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() + { + { 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) @@ -46,33 +136,553 @@ public void DaprExtendedErrorInfo_ThrowsRpcException_ShouldGetExtendedErrorInfo( } Assert.NotNull(result); - Assert.Single(result.Details); - Assert.Collection(result.Details, entry => + 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() { - Assert.IsAssignableFrom(entry); - Assert.IsType(entry); - }); + 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() + { + { 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() + { + { 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() + { + { 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); } - private static void ThrowsRpcBasedDaprException() + [Fact] + public void DaprExtendedErrorInfo_ThrowsPreconditionFailureRpcException_ShouldGetSingleDaprPreconditionFailureDetail() { + // Arrange + DaprExtendedErrorInfo result = null; var metadataEntry = new Google.Rpc.Status() { - Code = 1, - Message = "Status Message", + Code = statusCode, + Message = statusMessage, }; - PreconditionFailure failure = new(); + 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() + { + { 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() { { GrpcDetails, metadataEntry.ToByteArray() } }; - throw new DaprException("RpcBasedDaprException", new RpcException(status: new Grpc.Core.Status(StatusCode.FailedPrecondition, "PrecondtionFailure"), trailers: trailers)); + 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() + { + { 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() + { + { 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() + { + { 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() + { + { 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() + { + { 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); } } From 0d759c043d50cdc9ae317a95d8e32845e1b197bc Mon Sep 17 00:00:00 2001 From: Jev Date: Wed, 25 Dec 2024 23:38:24 +0000 Subject: [PATCH 05/10] Tidy up comments, add copyright to file. Signed-off-by: jev jacob@jev.org.uk Signed-off-by: jev --- .../Exceptions/DaprExceptionExtensions.cs | 23 ++++- .../Exceptions/DaprExtendedErrorDetail.cs | 94 +++++++++++-------- .../Exceptions/DaprExtendedErrorType.cs | 53 ++++++++--- .../Exceptions/ExtendedErrorDetailFactory.cs | 2 +- .../DaprExtendedErrorInfoTest.cs | 10 +- 5 files changed, 122 insertions(+), 60 deletions(-) diff --git a/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs b/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs index 78d1686a2..3ede04b97 100644 --- a/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs +++ b/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs @@ -1,19 +1,34 @@ -using System.Diagnostics.CodeAnalysis; +// ------------------------------------------------------------------------ +// 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 + /// Provides extension methods for . /// public static class DaprExceptionExtensions { private static string GrpcDetails = "grpc-status-details-bin"; /// - /// Attempt to retrieve from + /// 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; diff --git a/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs b/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs index 01e34b9ad..39c0e1b52 100644 --- a/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs +++ b/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs @@ -1,33 +1,46 @@ -namespace Dapr.Common.Exceptions +// ------------------------------------------------------------------------ +// 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 { /// - /// Base class of the Dapr extended error detail. + /// Abstract base class of the Dapr extended error detail. /// public abstract record DaprExtendedErrorDetail(DaprExtendedErrorType ErrorType); /// - /// An unrecognized detail. + /// Detail when the type url is unrecognized. /// - /// Type Url. + /// The unrecognized type url. public sealed record DaprUnrecognizedDetail(string TypeUrl) : DaprExtendedErrorDetail(DaprExtendedErrorType.Unrecognized); /// - /// A Debug Info detail. + /// Detail proving debugging information. /// - /// Stack Entries. - /// Detail. + /// Stack trace entries relating to error. + /// Further related debugging information. public sealed record DaprDebugInfoDetail(string[] StackEntries, string Detail) : DaprExtendedErrorDetail(DaprExtendedErrorType.DebugInfo); /// - /// A Precondition Violation. + /// A precondtion violation. /// - /// PreconditionType. - /// Subject. - /// A Description. + /// 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); /// - /// A Precondition Failure detail. + /// Detail relating to a failed precondition e.g user has not completed some required check. /// public sealed record DaprPreconditionFailureDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.PreconditionFailure) { @@ -38,32 +51,32 @@ public sealed record DaprPreconditionFailureDetail() : DaprExtendedErrorDetail(D } /// - /// Retry information. + /// Provides the time offset the client should use before retrying. /// /// Second offset. /// Nano offset. public sealed record DaprRetryDelay(long Seconds, int Nanos); /// - /// A Retry Info detail. + /// 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) { /// - /// Provides information of amount of time until retry should be attempted. + /// A . /// public DaprRetryDelay Delay = new(Seconds: 1, Nanos: default); } /// - /// A Quota Violation. + /// Further details relating to a quota violation. /// - /// The Subject. - /// A Description. + /// 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); /// - /// A Quota Failure detail. + /// Detail relating to a quota failure e.g reaching an API limit. /// public sealed record DaprQuotaFailureDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.QuotaFailure) { @@ -74,14 +87,14 @@ public sealed record DaprQuotaFailureDetail() : DaprExtendedErrorDetail(DaprExte } /// - /// Bad Request Field Violation + /// 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); /// - /// Dapr bad request details + /// Detail containing information related to a bad request from the client. /// public sealed record DaprBadRequestDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.BadRequest) { @@ -92,29 +105,29 @@ public sealed record DaprBadRequestDetail() : DaprExtendedErrorDetail(DaprExtend } /// - /// Request Info. + /// 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 RequestId. - /// ServingData. + /// 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); /// - /// Localized Message. + /// Detail containing a message that can be localized. /// - /// Locale. - /// Message. + /// The locale e.g 'en-US'. + /// A message to be localized. public sealed record DaprLocalizedMessageDetail(string Locale, string Message) : DaprExtendedErrorDetail(DaprExtendedErrorType.LocalizedMessage); /// - /// A link to help resources. + /// Contains a link to a help resource. /// - /// Url to help details. - /// A description. + /// 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); /// - /// Dapr help details + /// Detail containing links to further help resources. /// public sealed record DaprHelpDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.Help) { @@ -124,18 +137,21 @@ public sealed record DaprHelpDetail() : DaprExtendedErrorDetail(DaprExtendedErro public DaprHelpDetailLink[] Links { get; init; } = Array.Empty(); } - /// - /// Dapr resource info details. + /// 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); /// - /// Dapr error info details. + /// Detail containing information related to a server error. /// - /// Reason. - /// Domain. - /// Metadata. + /// 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/DaprExtendedErrorType.cs b/src/Dapr.Common/Exceptions/DaprExtendedErrorType.cs index 1356a2439..94904fa59 100644 --- a/src/Dapr.Common/Exceptions/DaprExtendedErrorType.cs +++ b/src/Dapr.Common/Exceptions/DaprExtendedErrorType.cs @@ -1,62 +1,89 @@ -namespace Dapr.Common.Exceptions +// ------------------------------------------------------------------------ +// 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. + /// 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 { /// - /// Unrecognized Extended Error Type. + /// Unrecognized extended error type. + /// Implemented by . /// Unrecognized, /// - /// Retry Info Detail Type. + /// Retry info detail type. + /// See . /// RetryInfo, /// - /// Debug Info Detail Type. + /// Debug info detail type. + /// See . /// DebugInfo, /// - /// Quote Failure Detail Type. + /// Quote failure detail type. + /// See . /// QuotaFailure, /// - /// Precondition Failure Detail Type. + /// Precondition failure detail type. + /// See . /// PreconditionFailure, /// - /// Request Info Detail Type. + /// Request info detail type. + /// See . /// RequestInfo, /// - /// Localized Message Detail Type. + /// Localized message detail type. + /// See . /// LocalizedMessage, /// - /// Bad Request Detail Type. + /// Bad request detail type. + /// See . /// BadRequest, /// - /// Error Info Detail Type. + /// Error info detail type. + /// See . /// ErrorInfo, /// - /// Help Detail Type. + /// Help detail type. + /// See . /// Help, /// - /// Resource Info Detail Type. + /// Resource info detail type. + /// See . /// ResourceInfo } diff --git a/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs b/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs index c05621bca..d546dcb8c 100644 --- a/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs +++ b/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs @@ -29,7 +29,7 @@ internal static class ExtendedErrorDetailFactory /// /// Create a new from an instance of . /// - /// The message to create the error detail from. + /// The detail metadata to create the error detail from. /// A new instance of internal static DaprExtendedErrorDetail CreateErrorDetail(Any metadata) { diff --git a/test/Dapr.Common.Test/DaprExtendedErrorInfoTest.cs b/test/Dapr.Common.Test/DaprExtendedErrorInfoTest.cs index 47ac64f39..dfebe7d42 100644 --- a/test/Dapr.Common.Test/DaprExtendedErrorInfoTest.cs +++ b/test/Dapr.Common.Test/DaprExtendedErrorInfoTest.cs @@ -16,7 +16,7 @@ public class DaprExtendedErrorInfoTest private static string GrpcDetails = "grpc-status-details-bin"; [Fact] - public void DaprExendedErrorInfo_ThrowsRpcDaprException_ExtendedErrorInfoNotNull() + public void DaprExendedErrorInfo_ThrowsRpcDaprException_ExtendedErrorInfoReturnsTrueAndNotNull() { // Arrange DaprExtendedErrorInfo result = null; @@ -52,7 +52,7 @@ public void DaprExendedErrorInfo_ThrowsRpcDaprException_ExtendedErrorInfoNotNull } [Fact] - public void DaprExendedErrorInfo_ThrowsNonRpcDaprException_ExtendedErrorInfoIsNull() + public void DaprExendedErrorInfo_ThrowsNonRpcDaprException_ExtendedErrorInfoReturnsFalseAndIsNull() { // Arrange DaprExtendedErrorInfo result = null; @@ -649,7 +649,11 @@ public void DaprExtendedErrorInfo_ThrowsRequestInfoRpcException_ShouldGetMultipl Message = statusMessage, }; - List expectedDetailTypes = new() { typeof(DaprResourceInfoDetail), typeof(DaprRequestInfoDetail) }; + List expectedDetailTypes = new() + { + typeof(DaprResourceInfoDetail), + typeof(DaprRequestInfoDetail), + }; RequestInfo requestInfo = new(); From f97a86b51f32a629cd9a4c217d266e2b7a63dc70 Mon Sep 17 00:00:00 2001 From: Jev Date: Wed, 25 Dec 2024 23:58:59 +0000 Subject: [PATCH 06/10] add and use constants, more docs tidy up. Signed-off-by: jev jacob@jev.org.uk Signed-off-by: jev --- .../Exceptions/DaprExceptionExtensions.cs | 4 +-- .../Exceptions/DaprExtendedErrorConstants.cs | 9 +++++++ .../Exceptions/DaprExtendedErrorDetail.cs | 4 +-- .../Exceptions/DaprExtendedErrorType.cs | 8 +++--- .../Exceptions/ExtendedErrorDetailFactory.cs | 4 +-- .../DaprExtendedErrorInfoTest.cs | 26 +++++++++---------- 6 files changed, 30 insertions(+), 25 deletions(-) create mode 100644 src/Dapr.Common/Exceptions/DaprExtendedErrorConstants.cs diff --git a/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs b/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs index 3ede04b97..3e2f45145 100644 --- a/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs +++ b/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs @@ -21,8 +21,6 @@ namespace Dapr.Common.Exceptions /// public static class DaprExceptionExtensions { - private static string GrpcDetails = "grpc-status-details-bin"; - /// /// Attempt to retrieve from . /// @@ -37,7 +35,7 @@ public static bool TryGetExtendedErrorInfo(this DaprException exception, [NotNul return false; } - var metadata = rpcException.Trailers.Get(GrpcDetails); + var metadata = rpcException.Trailers.Get(DaprExtendedErrorConstants.GrpcDetails); if (metadata is null) { diff --git a/src/Dapr.Common/Exceptions/DaprExtendedErrorConstants.cs b/src/Dapr.Common/Exceptions/DaprExtendedErrorConstants.cs new file mode 100644 index 000000000..514aecc35 --- /dev/null +++ b/src/Dapr.Common/Exceptions/DaprExtendedErrorConstants.cs @@ -0,0 +1,9 @@ +namespace Dapr.Common.Exceptions +{ + internal class DaprExtendedErrorConstants + { + public const string DaprErrorDetailTypeUrl = "type.googleapis.com/"; + + public const string GrpcDetails = "grpc-status-details-bin"; + } +} diff --git a/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs b/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs index 39c0e1b52..b0f1ccc10 100644 --- a/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs +++ b/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs @@ -22,7 +22,7 @@ public abstract record DaprExtendedErrorDetail(DaprExtendedErrorType ErrorType); /// Detail when the type url is unrecognized. /// /// The unrecognized type url. - public sealed record DaprUnrecognizedDetail(string TypeUrl) : DaprExtendedErrorDetail(DaprExtendedErrorType.Unrecognized); + public sealed record DaprUnknownDetail(string TypeUrl) : DaprExtendedErrorDetail(DaprExtendedErrorType.Unknown); /// /// Detail proving debugging information. @@ -76,7 +76,7 @@ public sealed record DaprRetryInfoDetail() : DaprExtendedErrorDetail(DaprExtende public sealed record DaprQuotaFailureViolation(string Subject, string Description); /// - /// Detail relating to a quota failure e.g reaching an API limit. + /// Detail relating to a quota failure e.g reaching API limit. /// public sealed record DaprQuotaFailureDetail() : DaprExtendedErrorDetail(DaprExtendedErrorType.QuotaFailure) { diff --git a/src/Dapr.Common/Exceptions/DaprExtendedErrorType.cs b/src/Dapr.Common/Exceptions/DaprExtendedErrorType.cs index 94904fa59..664f6beef 100644 --- a/src/Dapr.Common/Exceptions/DaprExtendedErrorType.cs +++ b/src/Dapr.Common/Exceptions/DaprExtendedErrorType.cs @@ -17,15 +17,15 @@ 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 ). + /// and is implemented by the Dapr runtime (see ). /// public enum DaprExtendedErrorType { /// - /// Unrecognized extended error type. - /// Implemented by . + /// Unknown extended error type. + /// Implemented by . /// - Unrecognized, + Unknown, /// /// Retry info detail type. diff --git a/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs b/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs index d546dcb8c..005616025 100644 --- a/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs +++ b/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs @@ -9,7 +9,7 @@ namespace Dapr.Common.Exceptions /// internal static class ExtendedErrorDetailFactory { - private const string DaprErrorTypeUrl = "type.googleapis.com/"; + private const string DaprErrorTypeUrl = DaprExtendedErrorConstants.DaprErrorDetailTypeUrl; private static Dictionary> extendedErrorTypeMapping = new() @@ -35,7 +35,7 @@ internal static DaprExtendedErrorDetail CreateErrorDetail(Any metadata) { if (!extendedErrorTypeMapping.TryGetValue(metadata.TypeUrl, out var create)) { - return new DaprUnrecognizedDetail(metadata.TypeUrl); + return new DaprUnknownDetail(metadata.TypeUrl); } return create.Invoke(metadata.Value); diff --git a/test/Dapr.Common.Test/DaprExtendedErrorInfoTest.cs b/test/Dapr.Common.Test/DaprExtendedErrorInfoTest.cs index dfebe7d42..b215bb600 100644 --- a/test/Dapr.Common.Test/DaprExtendedErrorInfoTest.cs +++ b/test/Dapr.Common.Test/DaprExtendedErrorInfoTest.cs @@ -13,8 +13,6 @@ public class DaprExtendedErrorInfoTest private static int statusCode = 1; private static string statusMessage = "Status Message"; - private static string GrpcDetails = "grpc-status-details-bin"; - [Fact] public void DaprExendedErrorInfo_ThrowsRpcDaprException_ExtendedErrorInfoReturnsTrueAndNotNull() { @@ -32,7 +30,7 @@ public void DaprExendedErrorInfo_ThrowsRpcDaprException_ExtendedErrorInfoReturns Metadata trailers = new() { - { GrpcDetails, metadataEntry.ToByteArray() } + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } }; var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "BadRequest"), trailers: trailers); @@ -119,7 +117,7 @@ public void DaprExtendedErrorInfo_ThrowsBadRequestRpcException_ShouldGetSingleDa Metadata trailers = new() { - { GrpcDetails, metadataEntry.ToByteArray() } + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } }; var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "BadRequest"), trailers: trailers); @@ -171,7 +169,7 @@ public void DaprExtendedErrorInfo_ThrowsLocalizedMessageRpcException_ShouldGetSi Metadata trailers = new() { - { GrpcDetails, metadataEntry.ToByteArray() } + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } }; var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "BadRequest"), trailers: trailers); @@ -220,7 +218,7 @@ public void DaprExtendedErrorInfo_ThrowRetryInfoRpcException_ShouldGetSingleDapr Metadata trailers = new() { - { GrpcDetails, metadataEntry.ToByteArray() } + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } }; var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.FailedPrecondition, "RetryInfo"), trailers: trailers); @@ -267,7 +265,7 @@ public void DaprExtendedErrorInfo_ThrowsDebugInfoRpcException_ShouldGetSingleDap Grpc.Core.Metadata trailers = new() { - { GrpcDetails, metadataEntry.ToByteArray() } + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } }; var rpcEx = new RpcException(status: new Grpc.Core.Status(Grpc.Core.StatusCode.FailedPrecondition, "DebugInfo"), trailers: trailers); @@ -323,7 +321,7 @@ public void DaprExtendedErrorInfo_ThrowsPreconditionFailureRpcException_ShouldGe Metadata trailers = new() { - { GrpcDetails, metadataEntry.ToByteArray() } + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } }; var rpcEx = new RpcException(status: new Grpc.Core.Status(Grpc.Core.StatusCode.FailedPrecondition, "PrecondtionFailure"), trailers: trailers); @@ -379,7 +377,7 @@ public void DaprExtendedErrorInfo_ThrowsHelpRpcException_ShouldGetSingleDaprHelp Metadata trailers = new() { - { GrpcDetails, metadataEntry.ToByteArray() } + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } }; var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "Help"), trailers: trailers); @@ -438,7 +436,7 @@ public void DaprExtendedErrorInfo_ThrowsResourceInfoRpcException_ShouldGetSingle Metadata trailers = new() { - { GrpcDetails, metadataEntry.ToByteArray() } + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } }; var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "ResourceInfo"), trailers: trailers); @@ -494,7 +492,7 @@ public void DaprExtendedErrorInfo_ThrowsQuotaFailureRpcException_ShouldGetSingle Metadata trailers = new() { - { GrpcDetails, metadataEntry.ToByteArray() } + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } }; var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "QuotaFailure"), trailers: trailers); @@ -556,7 +554,7 @@ public void DaprExtendedErrorInfo_ThrowsErrorInfoRpcException_ShouldGetSingleDap Metadata trailers = new() { - { GrpcDetails, metadataEntry.ToByteArray() } + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } }; var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "ErrorInfo"), trailers: trailers); @@ -612,7 +610,7 @@ public void DaprExtendedErrorInfo_ThrowsRequestInfoRpcException_ShouldGetSingleD Metadata trailers = new() { - { GrpcDetails, metadataEntry.ToByteArray() } + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } }; var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "RequestInfo"), trailers: trailers); @@ -665,7 +663,7 @@ public void DaprExtendedErrorInfo_ThrowsRequestInfoRpcException_ShouldGetMultipl Metadata trailers = new() { - { GrpcDetails, metadataEntry.ToByteArray() } + { DaprExtendedErrorConstants.GrpcDetails, metadataEntry.ToByteArray() } }; var rpcEx = new RpcException(status: new Grpc.Core.Status(StatusCode.Aborted, "RequestInfo"), trailers: trailers); From e475139ff7e4d7fa165a250de5a90a33f44f72c9 Mon Sep 17 00:00:00 2001 From: Jev Date: Thu, 26 Dec 2024 00:12:32 +0000 Subject: [PATCH 07/10] add initial docs pages for error handling in .net sdk. Signed-off-by: jev jacob@jev.org.uk Signed-off-by: jev --- .../en/dotnet-sdk-docs/dotnet-error-handling/_index.md | 7 +++++++ .../dotnet-error-handling/dotnet-richer-error-model.md | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 daprdocs/content/en/dotnet-sdk-docs/dotnet-error-handling/_index.md create mode 100644 daprdocs/content/en/dotnet-sdk-docs/dotnet-error-handling/dotnet-richer-error-model.md 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..f03bf6764 --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-error-handling/_index.md @@ -0,0 +1,7 @@ +--- +type: docs +title: "Error Handling .NET SDK" +linkTitle: "Error handling" +weight: 50000 +description: Learn about Dapr error handling in the .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..5cfea00ff --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-error-handling/dotnet-richer-error-model.md @@ -0,0 +1,7 @@ +--- +type: docs +title: "Richer Error Model in the .NET SDK" +linkTitle: "Richer error model" +weight: 59000 +description: Learn how to use the richer error model in the .NET SDK. +--- \ No newline at end of file From 587039a5097128ddea11987e51cd341e5c719146 Mon Sep 17 00:00:00 2001 From: Jev Date: Thu, 26 Dec 2024 13:23:59 +0000 Subject: [PATCH 08/10] write daprdocs detailing usage of extendedErrorInfo, rename vars signed-off-by: jev jacob@jev.org.uk Signed-off-by: jev --- .../dotnet-error-handling/_index.md | 4 +- .../dotnet-richer-error-model.md | 135 +++++++++++++++++- .../Exceptions/DaprExceptionExtensions.cs | 2 +- .../Exceptions/DaprExtendedErrorConstants.cs | 2 +- .../Exceptions/ExtendedErrorDetailFactory.cs | 14 +- 5 files changed, 144 insertions(+), 13 deletions(-) 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 index f03bf6764..4a2f065d7 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-error-handling/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-error-handling/_index.md @@ -1,7 +1,7 @@ --- type: docs -title: "Error Handling .NET SDK" +title: "Error Handling in the Dapr .NET SDK" linkTitle: "Error handling" weight: 50000 -description: Learn about Dapr error handling in the .NET SDK. +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 index 5cfea00ff..cff21ac4d 100644 --- 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 @@ -1,7 +1,138 @@ --- type: docs -title: "Richer Error Model in the .NET SDK" +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. ---- \ No newline at end of file +--- + +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); + } + } +} +``` + +## 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 reach, 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/Exceptions/DaprExceptionExtensions.cs b/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs index 3e2f45145..2f195d692 100644 --- a/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs +++ b/src/Dapr.Common/Exceptions/DaprExceptionExtensions.cs @@ -24,7 +24,7 @@ public static class DaprExceptionExtensions /// /// Attempt to retrieve from . /// - /// A Dapr exception.. + /// 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) diff --git a/src/Dapr.Common/Exceptions/DaprExtendedErrorConstants.cs b/src/Dapr.Common/Exceptions/DaprExtendedErrorConstants.cs index 514aecc35..bda382fc6 100644 --- a/src/Dapr.Common/Exceptions/DaprExtendedErrorConstants.cs +++ b/src/Dapr.Common/Exceptions/DaprExtendedErrorConstants.cs @@ -2,7 +2,7 @@ { internal class DaprExtendedErrorConstants { - public const string DaprErrorDetailTypeUrl = "type.googleapis.com/"; + public const string ErrorDetailTypeUrl = "type.googleapis.com/"; public const string GrpcDetails = "grpc-status-details-bin"; } diff --git a/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs b/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs index 005616025..3d5654e46 100644 --- a/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs +++ b/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs @@ -5,11 +5,11 @@ namespace Dapr.Common.Exceptions { /// - /// factory class. + /// factory. /// internal static class ExtendedErrorDetailFactory { - private const string DaprErrorTypeUrl = DaprExtendedErrorConstants.DaprErrorDetailTypeUrl; + private const string DaprErrorTypeUrl = DaprExtendedErrorConstants.ErrorDetailTypeUrl; private static Dictionary> extendedErrorTypeMapping = new() @@ -29,16 +29,16 @@ internal static class ExtendedErrorDetailFactory /// /// Create a new from an instance of . /// - /// The detail metadata to create the error detail from. + /// The serialized detail message to create the error detail from. /// A new instance of - internal static DaprExtendedErrorDetail CreateErrorDetail(Any metadata) + internal static DaprExtendedErrorDetail CreateErrorDetail(Any message) { - if (!extendedErrorTypeMapping.TryGetValue(metadata.TypeUrl, out var create)) + if (!extendedErrorTypeMapping.TryGetValue(message.TypeUrl, out var create)) { - return new DaprUnknownDetail(metadata.TypeUrl); + return new DaprUnknownDetail(message.TypeUrl); } - return create.Invoke(metadata.Value); + return create.Invoke(message.Value); } private static DaprRetryInfoDetail ToDaprRetryInfoDetail(ByteString data) From 6e81daea6960caa2f72710f0315ca0d46f11a568 Mon Sep 17 00:00:00 2001 From: jev Date: Fri, 27 Dec 2024 10:46:32 +0000 Subject: [PATCH 09/10] Address PR comments Signed-off-by: jev --- .../dotnet-richer-error-model.md | 4 +- .../Exceptions/DaprExtendedErrorConstants.cs | 31 +++++++++-- .../Exceptions/DaprExtendedErrorDetail.cs | 10 ++-- .../Exceptions/DaprExtendedErrorInfo.cs | 15 +++++- .../Exceptions/ExtendedErrorDetailFactory.cs | 51 ++++++++++--------- 5 files changed, 78 insertions(+), 33 deletions(-) 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 index cff21ac4d..5efd11fca 100644 --- 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 @@ -35,6 +35,8 @@ catch (DaprException daprEx) case ExtendedErrorType.ErrorInfo: Console.WriteLine(detail.Reason); Console.WriteLine(detail.Domain); + default: + Console.WriteLine(detail.TypeUrl); } } } @@ -83,7 +85,7 @@ Debugging information offered by the server. Contains `StackEntries` (a collecti ## QuotaFailure -Information relating to some quota that may have been reach, such as a daily usage limit on an API. It has one property `Violations`, +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 diff --git a/src/Dapr.Common/Exceptions/DaprExtendedErrorConstants.cs b/src/Dapr.Common/Exceptions/DaprExtendedErrorConstants.cs index bda382fc6..9507103bb 100644 --- a/src/Dapr.Common/Exceptions/DaprExtendedErrorConstants.cs +++ b/src/Dapr.Common/Exceptions/DaprExtendedErrorConstants.cs @@ -1,9 +1,34 @@ -namespace Dapr.Common.Exceptions +// ------------------------------------------------------------------------ +// 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 { - internal class DaprExtendedErrorConstants + /// + /// 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 index b0f1ccc10..17c10c20b 100644 --- a/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs +++ b/src/Dapr.Common/Exceptions/DaprExtendedErrorDetail.cs @@ -29,7 +29,7 @@ public sealed record DaprUnknownDetail(string TypeUrl) : DaprExtendedErrorDetail /// /// Stack trace entries relating to error. /// Further related debugging information. - public sealed record DaprDebugInfoDetail(string[] StackEntries, string Detail) : DaprExtendedErrorDetail(DaprExtendedErrorType.DebugInfo); + public sealed record DaprDebugInfoDetail(IReadOnlyCollection StackEntries, string Detail) : DaprExtendedErrorDetail(DaprExtendedErrorType.DebugInfo); /// /// A precondtion violation. @@ -47,7 +47,7 @@ public sealed record DaprPreconditionFailureDetail() : DaprExtendedErrorDetail(D /// /// Collection of . /// - public DaprPreconditionFailureViolation[] Violations { get; init; } = Array.Empty(); + public IReadOnlyCollection Violations { get; init; } = Array.Empty(); } /// @@ -83,7 +83,7 @@ public sealed record DaprQuotaFailureDetail() : DaprExtendedErrorDetail(DaprExte /// /// Collection of . /// - public DaprQuotaFailureViolation[] Violations { get; init; } = Array.Empty(); + public IReadOnlyCollection Violations { get; init; } = Array.Empty(); } /// @@ -101,7 +101,7 @@ public sealed record DaprBadRequestDetail() : DaprExtendedErrorDetail(DaprExtend /// /// Collection of . /// - public DaprBadRequestDetailFieldViolation[] FieldViolations { get; init; } = Array.Empty(); + public IReadOnlyCollection FieldViolations { get; init; } = Array.Empty(); } /// @@ -134,7 +134,7 @@ public sealed record DaprHelpDetail() : DaprExtendedErrorDetail(DaprExtendedErro /// /// Collection of . /// - public DaprHelpDetailLink[] Links { get; init; } = Array.Empty(); + public IReadOnlyCollection Links { get; init; } = Array.Empty(); } /// diff --git a/src/Dapr.Common/Exceptions/DaprExtendedErrorInfo.cs b/src/Dapr.Common/Exceptions/DaprExtendedErrorInfo.cs index 2f3fb9fc8..bcd337a38 100644 --- a/src/Dapr.Common/Exceptions/DaprExtendedErrorInfo.cs +++ b/src/Dapr.Common/Exceptions/DaprExtendedErrorInfo.cs @@ -1,4 +1,17 @@ -namespace Dapr.Common.Exceptions +// ------------------------------------------------------------------------ +// 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. diff --git a/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs b/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs index 3d5654e46..f81b6646d 100644 --- a/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs +++ b/src/Dapr.Common/Exceptions/ExtendedErrorDetailFactory.cs @@ -1,4 +1,17 @@ -using Google.Protobuf; +// ------------------------------------------------------------------------ +// 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; @@ -9,23 +22,6 @@ namespace Dapr.Common.Exceptions /// internal static class ExtendedErrorDetailFactory { - private const string DaprErrorTypeUrl = DaprExtendedErrorConstants.ErrorDetailTypeUrl; - - private static Dictionary> extendedErrorTypeMapping = - new() - { - { $"{DaprErrorTypeUrl}Google.rpc.ErrorInfo", ToDaprErrorInfoDetail }, - { $"{DaprErrorTypeUrl}Google.rpc.RetryInfo", ToDaprRetryInfoDetail }, - { $"{DaprErrorTypeUrl}Google.rpc.DebugInfo", ToDaprDebugInfoDetail }, - { $"{DaprErrorTypeUrl}Google.rpc.QuotaFailure", ToDaprQuotaFailureDetail }, - { $"{DaprErrorTypeUrl}Google.rpc.PreconditionFailure", ToDaprPreconditionFailureDetail }, - { $"{DaprErrorTypeUrl}Google.rpc.BadRequest", ToDaprBadRequestDetail }, - { $"{DaprErrorTypeUrl}Google.rpc.RequestInfo", ToDaprRequestInfoDetail }, - { $"{DaprErrorTypeUrl}Google.rpc.ResourceInfo", ToDaprResourceInfoDetail }, - { $"{DaprErrorTypeUrl}Google.rpc.Help", ToDaprHelpDetail }, - { $"{DaprErrorTypeUrl}Google.rpc.LocalizedMessage", ToDaprLocalizedMessageDetail }, - }; - /// /// Create a new from an instance of . /// @@ -33,12 +29,21 @@ internal static class ExtendedErrorDetailFactory /// A new instance of internal static DaprExtendedErrorDetail CreateErrorDetail(Any message) { - if (!extendedErrorTypeMapping.TryGetValue(message.TypeUrl, out var create)) + var data = message.Value; + return message.TypeUrl switch { - return new DaprUnknownDetail(message.TypeUrl); - } - - return create.Invoke(message.Value); + 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) From cf34edbaee86382dfc3532124f345086ad59e716 Mon Sep 17 00:00:00 2001 From: jev Date: Sat, 28 Dec 2024 18:50:19 +0000 Subject: [PATCH 10/10] pr comment; adjust weight Signed-off-by: jev --- .../content/en/dotnet-sdk-docs/dotnet-error-handling/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 4a2f065d7..fa87e2aac 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-error-handling/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-error-handling/_index.md @@ -2,6 +2,6 @@ type: docs title: "Error Handling in the Dapr .NET SDK" linkTitle: "Error handling" -weight: 50000 +weight: 90000 description: Learn about error handling in the Dapr.NET SDK. --- \ No newline at end of file