mirror of https://github.com/dapr/kit.git
458 lines
14 KiB
Go
458 lines
14 KiB
Go
/*
|
|
Copyright 2023 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.
|
|
*/
|
|
|
|
package errors
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"google.golang.org/genproto/googleapis/rpc/errdetails"
|
|
grpcCodes "google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/runtime/protoiface"
|
|
|
|
"github.com/dapr/kit/logger"
|
|
)
|
|
|
|
const (
|
|
Domain = "dapr.io"
|
|
|
|
errStringFormat = "api error: code = %s desc = %s"
|
|
|
|
typeGoogleAPI = "type.googleapis.com/"
|
|
)
|
|
|
|
var log = logger.NewLogger("dapr.kit")
|
|
|
|
// Error implements the Error interface and the interface that complies with "google.golang.org/grpc/status".FromError().
|
|
// It can be used to send errors to HTTP and gRPC servers, indicating the correct status code for each.
|
|
type Error struct {
|
|
// Added error details. To see available details see:
|
|
// https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto
|
|
details []proto.Message
|
|
|
|
// Status code for gRPC responses.
|
|
grpcCode grpcCodes.Code
|
|
|
|
// Status code for HTTP responses.
|
|
httpCode int
|
|
|
|
// Message is the human-readable error message.
|
|
message string
|
|
|
|
// Tag is a string identifying the error, used with HTTP responses only.
|
|
tag string
|
|
|
|
// Category is a string identifying the category of the error (i.e. "actor", "job", "pubsub), used for error code metrics only.
|
|
category string
|
|
}
|
|
|
|
// ErrorBuilder is used to build the error
|
|
type ErrorBuilder struct {
|
|
err Error
|
|
}
|
|
|
|
// errorJSON is used to build the error for the HTTP Methods json output
|
|
type errorJSON struct {
|
|
ErrorCode string `json:"errorCode"`
|
|
Message string `json:"message"`
|
|
Details []any `json:"details,omitempty"`
|
|
}
|
|
|
|
/**************************************
|
|
Error
|
|
**************************************/
|
|
|
|
// HTTPStatusCode gets the error http code
|
|
func (e *Error) HTTPStatusCode() int {
|
|
return e.httpCode
|
|
}
|
|
|
|
// GrpcStatusCode gets the error grpc code
|
|
func (e *Error) GrpcStatusCode() grpcCodes.Code {
|
|
return e.grpcCode
|
|
}
|
|
|
|
// ErrorCode returns the error code from the error, prioritizing the legacy Error.Tag, otherwise the ErrorInfo.Reason
|
|
func (e *Error) ErrorCode() string {
|
|
errorCode := e.tag
|
|
for _, detail := range e.details {
|
|
if _, ok := detail.(*errdetails.ErrorInfo); ok {
|
|
if _, errInfoReason := convertErrorDetails(detail, *e); errInfoReason != "" {
|
|
return errInfoReason
|
|
}
|
|
}
|
|
}
|
|
return errorCode
|
|
}
|
|
|
|
// Category returns the error code's category
|
|
func (e *Error) Category() string {
|
|
return e.category
|
|
}
|
|
|
|
// Error implements the error interface.
|
|
func (e Error) Error() string {
|
|
return e.String()
|
|
}
|
|
|
|
// String returns the string representation.
|
|
func (e Error) String() string {
|
|
return fmt.Sprintf(errStringFormat, e.grpcCode.String(), e.message)
|
|
}
|
|
|
|
// Is implements the interface that checks if the error matches the given one.
|
|
func (e *Error) Is(targetI error) bool {
|
|
// Ignore the message in the comparison because the target could have been formatted
|
|
var target *Error
|
|
if !errors.As(targetI, &target) {
|
|
return false
|
|
}
|
|
return e.tag == target.tag &&
|
|
e.grpcCode == target.grpcCode &&
|
|
e.httpCode == target.httpCode
|
|
}
|
|
|
|
// Allow details to be mutable and added to the error in runtime
|
|
func (e *Error) AddDetails(details ...proto.Message) *Error {
|
|
e.details = append(e.details, details...)
|
|
|
|
return e
|
|
}
|
|
|
|
// FromError takes in an error and returns back the kitError if it's that type under the hood
|
|
func FromError(err error) (*Error, bool) {
|
|
if err == nil {
|
|
return nil, false
|
|
}
|
|
|
|
var kitErr Error
|
|
if errors.As(err, &kitErr) {
|
|
return &kitErr, true
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
|
|
/*** GRPC Methods ***/
|
|
|
|
// GRPCStatus returns the gRPC status.Status object.
|
|
func (e Error) GRPCStatus() *status.Status {
|
|
stat := status.New(e.grpcCode, e.message)
|
|
|
|
// convert details from proto.Msg -> protoiface.MsgV1
|
|
var convertedDetails []protoiface.MessageV1
|
|
for _, detail := range e.details {
|
|
if v1, ok := detail.(protoiface.MessageV1); ok {
|
|
convertedDetails = append(convertedDetails, v1)
|
|
} else {
|
|
log.Debugf("Failed to convert error details: %s", detail)
|
|
}
|
|
}
|
|
|
|
if len(e.details) > 0 {
|
|
var err error
|
|
stat, err = stat.WithDetails(convertedDetails...)
|
|
if err != nil {
|
|
log.Debugf("Failed to add error details: %s to status: %s", err, stat)
|
|
}
|
|
}
|
|
|
|
return stat
|
|
}
|
|
|
|
/*** HTTP Methods ***/
|
|
|
|
// JSONErrorValue implements the errorResponseValue interface.
|
|
func (e Error) JSONErrorValue() []byte {
|
|
grpcStatus := e.GRPCStatus().Proto()
|
|
|
|
// Make httpCode human readable
|
|
|
|
// If there is no http legacy code, use the http status text
|
|
// This will get overwritten later if there is an ErrorInfo code
|
|
httpStatus := e.tag
|
|
if httpStatus == "" {
|
|
httpStatus = http.StatusText(e.httpCode)
|
|
}
|
|
|
|
errJSON := errorJSON{
|
|
ErrorCode: httpStatus,
|
|
Message: grpcStatus.GetMessage(),
|
|
}
|
|
|
|
// Handle err details
|
|
details := e.details
|
|
if len(details) > 0 {
|
|
errJSON.Details = make([]any, len(details))
|
|
for i, detail := range details {
|
|
detailMap, errorCode := convertErrorDetails(detail, e)
|
|
errJSON.Details[i] = detailMap
|
|
|
|
// If there is an errorCode, update the overall ErrorCode
|
|
if errorCode != "" {
|
|
errJSON.ErrorCode = errorCode
|
|
}
|
|
}
|
|
}
|
|
|
|
errBytes, err := json.Marshal(errJSON)
|
|
if err != nil {
|
|
errJSON, _ := json.Marshal(fmt.Sprintf("failed to encode proto to JSON: %v", err))
|
|
return errJSON
|
|
}
|
|
return errBytes
|
|
}
|
|
|
|
func convertErrorDetails(detail any, e Error) (map[string]interface{}, string) {
|
|
// cast to interface to be able to do type switch
|
|
// over all possible error_details defined
|
|
// https://github.com/googleapis/go-genproto/blob/main/googleapis/rpc/errdetails/error_details.pb.go
|
|
switch typedDetail := detail.(type) {
|
|
case *errdetails.ErrorInfo:
|
|
desc := typedDetail.ProtoReflect().Descriptor()
|
|
detailMap := map[string]interface{}{
|
|
"@type": typeGoogleAPI + desc.FullName(),
|
|
"reason": typedDetail.GetReason(),
|
|
"domain": typedDetail.GetDomain(),
|
|
"metadata": typedDetail.GetMetadata(),
|
|
}
|
|
var errorCode string
|
|
// If there is an ErrorInfo Reason, but no legacy Tag code, use the ErrorInfo Reason as the error code
|
|
if e.tag == "" && typedDetail.GetReason() != "" {
|
|
errorCode = typedDetail.GetReason()
|
|
}
|
|
return detailMap, errorCode
|
|
case *errdetails.RetryInfo:
|
|
desc := typedDetail.ProtoReflect().Descriptor()
|
|
detailMap := map[string]interface{}{
|
|
"@type": typeGoogleAPI + desc.FullName(),
|
|
"retry_delay": typedDetail.GetRetryDelay(),
|
|
}
|
|
return detailMap, ""
|
|
case *errdetails.DebugInfo:
|
|
desc := typedDetail.ProtoReflect().Descriptor()
|
|
detailMap := map[string]interface{}{
|
|
"@type": typeGoogleAPI + desc.FullName(),
|
|
"stack_entries": typedDetail.GetStackEntries(),
|
|
"detail": typedDetail.GetDetail(),
|
|
}
|
|
return detailMap, ""
|
|
case *errdetails.QuotaFailure:
|
|
desc := typedDetail.ProtoReflect().Descriptor()
|
|
detailMap := map[string]interface{}{
|
|
"@type": typeGoogleAPI + desc.FullName(),
|
|
"violations": typedDetail.GetViolations(),
|
|
}
|
|
return detailMap, ""
|
|
case *errdetails.PreconditionFailure:
|
|
desc := typedDetail.ProtoReflect().Descriptor()
|
|
detailMap := map[string]interface{}{
|
|
"@type": typeGoogleAPI + desc.FullName(),
|
|
"violations": typedDetail.GetViolations(),
|
|
}
|
|
return detailMap, ""
|
|
case *errdetails.BadRequest:
|
|
desc := typedDetail.ProtoReflect().Descriptor()
|
|
detailMap := map[string]interface{}{
|
|
"@type": typeGoogleAPI + desc.FullName(),
|
|
"field_violations": typedDetail.GetFieldViolations(),
|
|
}
|
|
return detailMap, ""
|
|
case *errdetails.RequestInfo:
|
|
desc := typedDetail.ProtoReflect().Descriptor()
|
|
detailMap := map[string]interface{}{
|
|
"@type": typeGoogleAPI + desc.FullName(),
|
|
"request_id": typedDetail.GetRequestId(),
|
|
"serving_data": typedDetail.GetServingData(),
|
|
}
|
|
return detailMap, ""
|
|
case *errdetails.ResourceInfo:
|
|
desc := typedDetail.ProtoReflect().Descriptor()
|
|
detailMap := map[string]interface{}{
|
|
"@type": typeGoogleAPI + desc.FullName(),
|
|
"resource_type": typedDetail.GetResourceType(),
|
|
"resource_name": typedDetail.GetResourceName(),
|
|
"owner": typedDetail.GetOwner(),
|
|
"description": typedDetail.GetDescription(),
|
|
}
|
|
return detailMap, ""
|
|
case *errdetails.Help:
|
|
desc := typedDetail.ProtoReflect().Descriptor()
|
|
detailMap := map[string]interface{}{
|
|
"@type": typeGoogleAPI + desc.FullName(),
|
|
"links": typedDetail.GetLinks(),
|
|
}
|
|
return detailMap, ""
|
|
case *errdetails.LocalizedMessage:
|
|
desc := typedDetail.ProtoReflect().Descriptor()
|
|
detailMap := map[string]interface{}{
|
|
"@type": typeGoogleAPI + desc.FullName(),
|
|
"locale": typedDetail.GetLocale(),
|
|
"message": typedDetail.GetMessage(),
|
|
}
|
|
return detailMap, ""
|
|
case *errdetails.QuotaFailure_Violation:
|
|
desc := typedDetail.ProtoReflect().Descriptor()
|
|
detailMap := map[string]interface{}{
|
|
"@type": typeGoogleAPI + desc.FullName(),
|
|
"subject": typedDetail.GetSubject(),
|
|
"description": typedDetail.GetDescription(),
|
|
}
|
|
return detailMap, ""
|
|
case *errdetails.PreconditionFailure_Violation:
|
|
desc := typedDetail.ProtoReflect().Descriptor()
|
|
detailMap := map[string]interface{}{
|
|
"@type": typeGoogleAPI + desc.FullName(),
|
|
"subject": typedDetail.GetSubject(),
|
|
"description": typedDetail.GetDescription(),
|
|
"type": typedDetail.GetType(),
|
|
}
|
|
return detailMap, ""
|
|
case *errdetails.BadRequest_FieldViolation:
|
|
desc := typedDetail.ProtoReflect().Descriptor()
|
|
detailMap := map[string]interface{}{
|
|
"@type": typeGoogleAPI + desc.FullName(),
|
|
"field": typedDetail.GetField(),
|
|
"description": typedDetail.GetDescription(),
|
|
}
|
|
return detailMap, ""
|
|
case *errdetails.Help_Link:
|
|
desc := typedDetail.ProtoReflect().Descriptor()
|
|
detailMap := map[string]interface{}{
|
|
"@type": typeGoogleAPI + desc.FullName(),
|
|
"description": typedDetail.GetDescription(),
|
|
"url": typedDetail.GetUrl(),
|
|
}
|
|
return detailMap, ""
|
|
default:
|
|
log.Debugf("Failed to convert error details due to incorrect type. \nSee types here: https://github.com/googleapis/googleapis/blob/master/google/rpc/error_details.proto. \nDetail: %s", detail)
|
|
// Handle unknown detail types
|
|
unknownDetail := map[string]interface{}{
|
|
"unknownDetailType": fmt.Sprintf("%T", typedDetail),
|
|
"unknownDetails": fmt.Sprintf("%#v", typedDetail),
|
|
}
|
|
return unknownDetail, ""
|
|
}
|
|
}
|
|
|
|
/**************************************
|
|
ErrorBuilder
|
|
**************************************/
|
|
|
|
// NewBuilder create a new ErrorBuilder using the supplied required error fields
|
|
func NewBuilder(grpcCode grpcCodes.Code, httpCode int, message string, tag string, category string) *ErrorBuilder {
|
|
return &ErrorBuilder{
|
|
err: Error{
|
|
details: make([]proto.Message, 0),
|
|
grpcCode: grpcCode,
|
|
httpCode: httpCode,
|
|
message: message,
|
|
tag: tag,
|
|
category: category,
|
|
},
|
|
}
|
|
}
|
|
|
|
// WithResourceInfo is used to pass ResourceInfo error details to the Error struct.
|
|
func (b *ErrorBuilder) WithResourceInfo(resourceType string, resourceName string, owner string, description string) *ErrorBuilder {
|
|
resourceInfo := &errdetails.ResourceInfo{
|
|
ResourceType: resourceType,
|
|
ResourceName: resourceName,
|
|
Owner: owner,
|
|
Description: description,
|
|
}
|
|
|
|
b.err.details = append(b.err.details, resourceInfo)
|
|
|
|
return b
|
|
}
|
|
|
|
// WithHelpLink is used to pass HelpLink error details to the Error struct.
|
|
func (b *ErrorBuilder) WithHelpLink(url string, description string) *ErrorBuilder {
|
|
link := errdetails.Help_Link{
|
|
Description: description,
|
|
Url: url,
|
|
}
|
|
var links []*errdetails.Help_Link
|
|
links = append(links, &link)
|
|
|
|
help := &errdetails.Help{Links: links}
|
|
b.err.details = append(b.err.details, help)
|
|
|
|
return b
|
|
}
|
|
|
|
// WithHelp is used to pass Help error details to the Error struct.
|
|
func (b *ErrorBuilder) WithHelp(links []*errdetails.Help_Link) *ErrorBuilder {
|
|
b.err.details = append(b.err.details, &errdetails.Help{Links: links})
|
|
|
|
return b
|
|
}
|
|
|
|
// WithErrorInfo adds error information to the Error struct.
|
|
func (b *ErrorBuilder) WithErrorInfo(reason string, metadata map[string]string) *ErrorBuilder {
|
|
errorInfo := &errdetails.ErrorInfo{
|
|
Domain: Domain,
|
|
Reason: reason,
|
|
Metadata: metadata,
|
|
}
|
|
b.err.details = append(b.err.details, errorInfo)
|
|
|
|
return b
|
|
}
|
|
|
|
// WithFieldViolation is used to pass FieldViolation error details to the Error struct.
|
|
func (b *ErrorBuilder) WithFieldViolation(fieldName string, msg string) *ErrorBuilder {
|
|
br := &errdetails.BadRequest{
|
|
FieldViolations: []*errdetails.BadRequest_FieldViolation{{
|
|
Field: fieldName,
|
|
Description: msg,
|
|
}},
|
|
}
|
|
|
|
b.err.details = append(b.err.details, br)
|
|
|
|
return b
|
|
}
|
|
|
|
// WithDetails is used to pass any error details to the Error struct.
|
|
func (b *ErrorBuilder) WithDetails(details ...proto.Message) *ErrorBuilder {
|
|
b.err.details = append(b.err.details, details...)
|
|
|
|
return b
|
|
}
|
|
|
|
// Build builds our error
|
|
func (b *ErrorBuilder) Build() error {
|
|
// Check for ErrorInfo, since it's required per the proposal
|
|
containsErrorInfo := false
|
|
for _, detail := range b.err.details {
|
|
if _, ok := detail.(*errdetails.ErrorInfo); ok {
|
|
containsErrorInfo = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !containsErrorInfo {
|
|
log.Errorf("Must include ErrorInfo in error details. Error: %s", b.err.Error())
|
|
panic("Must include ErrorInfo in error details.")
|
|
}
|
|
|
|
return b.err
|
|
}
|