diff --git a/errors/codes.go b/errors/codes.go deleted file mode 100644 index 64bb1179f..000000000 --- a/errors/codes.go +++ /dev/null @@ -1,317 +0,0 @@ -package errors - -import ( - "errors" - "fmt" -) - -// Cancelled The operation was cancelled, typically by the caller. -// HTTP Mapping: 499 Client Closed Request -func Cancelled(reason, format string, a ...interface{}) error { - return &StatusError{ - Code: 1, - Reason: reason, - Message: fmt.Sprintf(format, a...), - } -} - -// IsCancelled determines if err is an error which indicates a cancelled error. -// It supports wrapped errors. -func IsCancelled(err error) bool { - if se := new(StatusError); errors.As(err, &se) { - return se.Code == 1 - } - return false -} - -// Unknown error. -// HTTP Mapping: 500 Internal Server Error -func Unknown(reason, format string, a ...interface{}) error { - return &StatusError{ - Code: 2, - Reason: reason, - Message: fmt.Sprintf(format, a...), - } -} - -// IsUnknown determines if err is an error which indicates a unknown error. -// It supports wrapped errors. -func IsUnknown(err error) bool { - if se := new(StatusError); errors.As(err, &se) { - return se.Code == 2 - } - return false -} - -// InvalidArgument The client specified an invalid argument. -// HTTP Mapping: 400 Bad Request -func InvalidArgument(reason, format string, a ...interface{}) error { - return &StatusError{ - Code: 3, - Reason: reason, - Message: fmt.Sprintf(format, a...), - } -} - -// IsInvalidArgument determines if err is an error which indicates an invalid argument error. -// It supports wrapped errors. -func IsInvalidArgument(err error) bool { - if se := new(StatusError); errors.As(err, &se) { - return se.Code == 3 - } - return false -} - -// DeadlineExceeded The deadline expired before the operation could complete. -// HTTP Mapping: 504 Gateway Timeout -func DeadlineExceeded(reason, format string, a ...interface{}) error { - return &StatusError{ - Code: 4, - Reason: reason, - Message: fmt.Sprintf(format, a...), - } -} - -// IsDeadlineExceeded determines if err is an error which indicates a deadline exceeded error. -// It supports wrapped errors. -func IsDeadlineExceeded(err error) bool { - if se := new(StatusError); errors.As(err, &se) { - return se.Code == 4 - } - return false -} - -// NotFound Some requested entity (e.g., file or directory) was not found. -// HTTP Mapping: 404 Not Found -func NotFound(reason, format string, a ...interface{}) error { - return &StatusError{ - Code: 5, - Reason: reason, - Message: fmt.Sprintf(format, a...), - } -} - -// IsNotFound determines if err is an error which indicates a not found error. -// It supports wrapped errors. -func IsNotFound(err error) bool { - if se := new(StatusError); errors.As(err, &se) { - return se.Code == 5 - } - return false -} - -// AlreadyExists The entity that a client attempted to create (e.g., file or directory) already exists. -// HTTP Mapping: 409 Conflict -func AlreadyExists(reason, format string, a ...interface{}) error { - return &StatusError{ - Code: 6, - Reason: reason, - Message: fmt.Sprintf(format, a...), - } -} - -// IsAlreadyExists determines if err is an error which indicates a already exsits error. -// It supports wrapped errors. -func IsAlreadyExists(err error) bool { - if se := new(StatusError); errors.As(err, &se) { - return se.Code == 6 - } - return false -} - -// PermissionDenied The caller does not have permission to execute the specified operation. -// HTTP Mapping: 403 Forbidden -func PermissionDenied(reason, format string, a ...interface{}) error { - return &StatusError{ - Code: 7, - Reason: reason, - Message: fmt.Sprintf(format, a...), - } -} - -// IsPermissionDenied determines if err is an error which indicates a permission denied error. -// It supports wrapped errors. -func IsPermissionDenied(err error) bool { - if se := new(StatusError); errors.As(err, &se) { - return se.Code == 7 - } - return false -} - -// ResourceExhausted Some resource has been exhausted, perhaps a per-user quota, or -// perhaps the entire file system is out of space. -// HTTP Mapping: 429 Too Many Requests -func ResourceExhausted(reason, format string, a ...interface{}) error { - return &StatusError{ - Code: 8, - Reason: reason, - Message: fmt.Sprintf(format, a...), - } -} - -// IsResourceExhausted determines if err is an error which indicates a resource exhausted error. -// It supports wrapped errors. -func IsResourceExhausted(err error) bool { - if se := new(StatusError); errors.As(err, &se) { - return se.Code == 8 - } - return false -} - -// FailedPrecondition The operation was rejected because the system is not in a state -// required for the operation's execution. -// HTTP Mapping: 400 Bad Request -func FailedPrecondition(reason, format string, a ...interface{}) error { - return &StatusError{ - Code: 9, - Reason: reason, - Message: fmt.Sprintf(format, a...), - } -} - -// IsFailedPrecondition determines if err is an error which indicates a failed precondition error. -// It supports wrapped errors. -func IsFailedPrecondition(err error) bool { - if se := new(StatusError); errors.As(err, &se) { - return se.Code == 9 - } - return false -} - -// Aborted The operation was aborted, typically due to a concurrency issue such as -// a sequencer check failure or transaction abort. -// HTTP Mapping: 409 Conflict -func Aborted(reason, format string, a ...interface{}) error { - return &StatusError{ - Code: 10, - Reason: reason, - Message: fmt.Sprintf(format, a...), - } -} - -// IsAborted determines if err is an error which indicates an aborted error. -// It supports wrapped errors. -func IsAborted(err error) bool { - if se := new(StatusError); errors.As(err, &se) { - return se.Code == 10 - } - return false -} - -// OutOfRange The operation was attempted past the valid range. E.g., seeking or -// reading past end-of-file. -// HTTP Mapping: 400 Bad Request -func OutOfRange(reason, format string, a ...interface{}) error { - return &StatusError{ - Code: 11, - Reason: reason, - Message: fmt.Sprintf(format, a...), - } -} - -// IsOutOfRange determines if err is an error which indicates a out of range error. -// It supports wrapped errors. -func IsOutOfRange(err error) bool { - if se := new(StatusError); errors.As(err, &se) { - return se.Code == 11 - } - return false -} - -// Unimplemented The operation is not implemented or is not supported/enabled in this service. -// HTTP Mapping: 501 Not Implemented -func Unimplemented(reason, format string, a ...interface{}) error { - return &StatusError{ - Code: 12, - Reason: reason, - Message: fmt.Sprintf(format, a...), - } -} - -// IsUnimplemented determines if err is an error which indicates a unimplemented error. -// It supports wrapped errors. -func IsUnimplemented(err error) bool { - if se := new(StatusError); errors.As(err, &se) { - return se.Code == 12 - } - return false -} - -// Internal This means that some invariants expected by the -// underlying system have been broken. This error code is reserved -// for serious errors. -// -// HTTP Mapping: 500 Internal Server Error -func Internal(reason, format string, a ...interface{}) error { - return &StatusError{ - Code: 13, - Reason: reason, - Message: fmt.Sprintf(format, a...), - } -} - -// IsInternal determines if err is an error which indicates an internal server error. -// It supports wrapped errors. -func IsInternal(err error) bool { - if se := new(StatusError); errors.As(err, &se) { - return se.Code == 13 - } - return false -} - -// Unavailable The service is currently unavailable. -// HTTP Mapping: 503 Service Unavailable -func Unavailable(reason, format string, a ...interface{}) error { - return &StatusError{ - Code: 14, - Reason: reason, - Message: fmt.Sprintf(format, a...), - } -} - -// IsUnavailable determines if err is an error which indicates a unavailable error. -// It supports wrapped errors. -func IsUnavailable(err error) bool { - if se := new(StatusError); errors.As(err, &se) { - return se.Code == 14 - } - return false -} - -// DataLoss Unrecoverable data loss or corruption. -// HTTP Mapping: 500 Internal Server Error -func DataLoss(reason, format string, a ...interface{}) error { - return &StatusError{ - Code: 15, - Reason: reason, - Message: fmt.Sprintf(format, a...), - } -} - -// IsDataLoss determines if err is an error which indicates a data loss error. -// It supports wrapped errors. -func IsDataLoss(err error) bool { - if se := new(StatusError); errors.As(err, &se) { - return se.Code == 15 - } - return false -} - -// Unauthorized The request does not have valid authentication credentials for the operation. -// HTTP Mapping: 401 Unauthorized -func Unauthorized(reason, format string, a ...interface{}) error { - return &StatusError{ - Code: 16, - Reason: reason, - Message: fmt.Sprintf(format, a...), - } -} - -// IsUnauthorized determines if err is an error which indicates a unauthorized error. -// It supports wrapped errors. -func IsUnauthorized(err error) bool { - if se := new(StatusError); errors.As(err, &se) { - return se.Code == 16 - } - return false -} diff --git a/errors/errors.go b/errors/errors.go index ad991020c..6c921c3e4 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -7,110 +7,83 @@ import ( ) const ( - // UnknownReason is unknown reason for error info. - UnknownReason = "" // SupportPackageIsVersion1 this constant should not be referenced by any other code. SupportPackageIsVersion1 = true ) -var _ error = (*StatusError)(nil) +// Error contains an error response from the server. +// For more details see https://github.com/go-kratos/kratos/issues/858. +type Error struct { + Code int `json:"code"` + Domain string `json:"domain"` + Reason string `json:"reason"` + Message string `json:"message"` + Metadata map[string]string `json:"metadata"` +} -// StatusError contains an error response from the server. -type StatusError = Status +// WithMetadata with an MD formed by the mapping of key, value. +func (e *Error) WithMetadata(md map[string]string) *Error { + err := *e + err.Metadata = md + return &err +} // Is matches each error in the chain with the target value. -func (e *StatusError) Is(target error) bool { - err, ok := target.(*StatusError) - if ok { - return e.Code == err.Code +func (e *Error) Is(err error) bool { + if target := new(Error); errors.As(err, &target) { + return target.Code == e.Code && target.Reason == e.Reason } return false } -// HTTPStatus returns the Status represented by se. -func (e *StatusError) HTTPStatus() int { - switch e.Code { - case 0: - return http.StatusOK - case 1: - return http.StatusInternalServerError - case 2: - return http.StatusInternalServerError - case 3: - return http.StatusBadRequest - case 4: - return http.StatusRequestTimeout - case 5: - return http.StatusNotFound - case 6: - return http.StatusConflict - case 7: - return http.StatusForbidden - case 8: - return http.StatusTooManyRequests - case 9: - return http.StatusPreconditionFailed - case 10: - return http.StatusConflict - case 11: - return http.StatusBadRequest - case 12: - return http.StatusNotImplemented - case 13: - return http.StatusInternalServerError - case 14: - return http.StatusServiceUnavailable - case 15: - return http.StatusInternalServerError - case 16: - return http.StatusUnauthorized - default: - return http.StatusInternalServerError - } +func (e *Error) Error() string { + return fmt.Sprintf("error: code = %d domain = %s reason = %s message = %s metadata = %v", e.Code, e.Domain, e.Reason, e.Message, e.Metadata) } -func (e *StatusError) Error() string { - return fmt.Sprintf("error: code = %d reason = %s message = %s details = %+v", e.Code, e.Reason, e.Message, e.Details) -} - -// Error returns a Status representing c and msg. -func Error(code int32, reason, message string) error { - return &StatusError{ +// New returns an error object for the code, message and error info. +func New(code int, reason, message string) *Error { + return &Error{ Code: code, Reason: reason, Message: message, } } -// Errorf returns New(c, fmt.Sprintf(format, a...)). -func Errorf(code int32, reason, format string, a ...interface{}) error { - return Error(code, reason, fmt.Sprintf(format, a...)) -} - -// Code returns the status code. -func Code(err error) int32 { +// Code returns the code for a particular error. +// It supports wrapped errors. +func Code(err error) int { if err == nil { - return 0 // ok + return http.StatusOK } - if se := new(StatusError); errors.As(err, &se) { - return se.Code + if target := new(Error); errors.As(err, &target) { + return target.Code } - return 2 // unknown + return http.StatusInternalServerError } -// Reason returns the status for a particular error. +// Domain returns the domain for a particular error. +// It supports wrapped errors. +func Domain(err error) string { + if target := new(Error); errors.As(err, &target) { + return target.Domain + } + return "" +} + +// Reason returns the reason for a particular error. // It supports wrapped errors. func Reason(err error) string { - if se := new(StatusError); errors.As(err, &se) { - return se.Reason + if target := new(Error); errors.As(err, &target) { + return target.Reason } - return UnknownReason + return "" } -// FromError returns status error. -func FromError(err error) (*StatusError, bool) { - if se := new(StatusError); errors.As(err, &se) { - return se, true +// FromError try to convert an error to *Error. +// It supports wrapped errors. +func FromError(err error) *Error { + if target := new(Error); errors.As(err, &target) { + return target } - return nil, false + return New(http.StatusInternalServerError, "", err.Error()) } diff --git a/errors/errors.pb.go b/errors/errors.pb.go deleted file mode 100644 index 3b142ba34..000000000 --- a/errors/errors.pb.go +++ /dev/null @@ -1,187 +0,0 @@ -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.25.0 -// protoc v3.13.0 -// source: errors.proto - -package errors - -import ( - proto "github.com/golang/protobuf/proto" - any "github.com/golang/protobuf/ptypes/any" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -// This is a compile-time assertion that a sufficiently up-to-date version -// of the legacy proto package is being used. -const _ = proto.ProtoPackageIsVersion4 - -type Status struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` - Reason string `protobuf:"bytes,2,opt,name=reason,proto3" json:"reason,omitempty"` - Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"` - Details []*any.Any `protobuf:"bytes,4,rep,name=details,proto3" json:"details,omitempty"` -} - -func (x *Status) Reset() { - *x = Status{} - if protoimpl.UnsafeEnabled { - mi := &file_errors_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *Status) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*Status) ProtoMessage() {} - -func (x *Status) ProtoReflect() protoreflect.Message { - mi := &file_errors_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use Status.ProtoReflect.Descriptor instead. -func (*Status) Descriptor() ([]byte, []int) { - return file_errors_proto_rawDescGZIP(), []int{0} -} - -func (x *Status) GetCode() int32 { - if x != nil { - return x.Code - } - return 0 -} - -func (x *Status) GetReason() string { - if x != nil { - return x.Reason - } - return "" -} - -func (x *Status) GetMessage() string { - if x != nil { - return x.Message - } - return "" -} - -func (x *Status) GetDetails() []*any.Any { - if x != nil { - return x.Details - } - return nil -} - -var File_errors_proto protoreflect.FileDescriptor - -var file_errors_proto_rawDesc = []byte{ - 0x0a, 0x0c, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, - 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x1a, 0x19, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, - 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x7e, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x18, - 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x64, 0x65, 0x74, 0x61, - 0x69, 0x6c, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, - 0x07, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x42, 0x62, 0x0a, 0x11, 0x64, 0x65, 0x76, 0x2e, - 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2e, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x42, 0x0b, 0x45, - 0x72, 0x72, 0x6f, 0x72, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x2c, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x2d, 0x6b, 0x72, 0x61, 0x74, - 0x6f, 0x73, 0x2f, 0x6b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x2f, 0x76, 0x32, 0x2f, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x73, 0x3b, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0xf8, 0x01, 0x01, 0xa2, 0x02, 0x0c, - 0x4b, 0x72, 0x61, 0x74, 0x6f, 0x73, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, -} - -var ( - file_errors_proto_rawDescOnce sync.Once - file_errors_proto_rawDescData = file_errors_proto_rawDesc -) - -func file_errors_proto_rawDescGZIP() []byte { - file_errors_proto_rawDescOnce.Do(func() { - file_errors_proto_rawDescData = protoimpl.X.CompressGZIP(file_errors_proto_rawDescData) - }) - return file_errors_proto_rawDescData -} - -var file_errors_proto_msgTypes = make([]protoimpl.MessageInfo, 1) -var file_errors_proto_goTypes = []interface{}{ - (*Status)(nil), // 0: kratos.errors.Status - (*any.Any)(nil), // 1: google.protobuf.Any -} -var file_errors_proto_depIdxs = []int32{ - 1, // 0: kratos.errors.Status.details:type_name -> google.protobuf.Any - 1, // [1:1] is the sub-list for method output_type - 1, // [1:1] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name -} - -func init() { file_errors_proto_init() } -func file_errors_proto_init() { - if File_errors_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_errors_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Status); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_errors_proto_rawDesc, - NumEnums: 0, - NumMessages: 1, - NumExtensions: 0, - NumServices: 0, - }, - GoTypes: file_errors_proto_goTypes, - DependencyIndexes: file_errors_proto_depIdxs, - MessageInfos: file_errors_proto_msgTypes, - }.Build() - File_errors_proto = out.File - file_errors_proto_rawDesc = nil - file_errors_proto_goTypes = nil - file_errors_proto_depIdxs = nil -} diff --git a/errors/errors.proto b/errors/errors.proto deleted file mode 100644 index 653bbf961..000000000 --- a/errors/errors.proto +++ /dev/null @@ -1,19 +0,0 @@ -syntax = "proto3"; - -package kratos.errors; - -import "google/protobuf/any.proto"; - -option cc_enable_arenas = true; -option go_package = "github.com/go-kratos/kratos/v2/errors;errors"; -option java_multiple_files = true; -option java_outer_classname = "ErrorsProto"; -option java_package = "com.github.kratos.errors"; -option objc_class_prefix = "KratosErrors"; - -message Status { - int32 code = 1; - string reason = 2; - string message = 3; - repeated google.protobuf.Any details = 4; -} diff --git a/errors/errors_test.go b/errors/errors_test.go index 4c66b2934..ba7162cbc 100644 --- a/errors/errors_test.go +++ b/errors/errors_test.go @@ -6,49 +6,45 @@ import ( "testing" ) -func TestErrorsMatch(t *testing.T) { - s := &StatusError{Code: 1} - st := &StatusError{Code: 2} +func TestError(t *testing.T) { + var ( + base *Error + ) + err := New(400, "reason", "message") + err2 := New(400, "reason", "message") + err3 := err.WithMetadata(map[string]string{ + "foo": "bar", + }) + werr := fmt.Errorf("wrap %w", err) - if errors.Is(s, st) { - t.Errorf("error is not match: %+v -> %+v", s, st) + if errors.Is(err, new(Error)) { + t.Errorf("should not be equal: %v", err) + } + if !errors.Is(werr, err) { + t.Errorf("should be equal: %v", err) + } + if !errors.Is(werr, err2) { + t.Errorf("should be equal: %v", err) } - s.Code = 1 - st.Code = 1 - if !errors.Is(s, st) { - t.Errorf("error is not match: %+v -> %+v", s, st) + if !errors.As(err, &base) { + t.Errorf("should be matchs: %v", err) + } + if !IsBadRequest(err) { + t.Errorf("should be matchs: %v", err) } - s.Reason = "test_reason" - s.Reason = "test_reason" - - if !errors.Is(s, st) { - t.Errorf("error is not match: %+v -> %+v", s, st) + if code := Code(err); code != err2.Code { + t.Errorf("got %d want: %s", code, err) + } + if domain := Domain(err); domain != err2.Domain { + t.Errorf("got %s want: %s", domain, err) + } + if reason := Reason(err); reason != err3.Reason { + t.Errorf("got %s want: %s", reason, err) } - if Reason(s) != "test_reason" { - t.Errorf("error is not match: %+v -> %+v", s, st) - } -} - -func TestErrorIs(t *testing.T) { - err1 := &StatusError{Code: 1} - t.Log(err1) - err2 := fmt.Errorf("wrap : %w", err1) - t.Log(err2) - - if !(errors.Is(err2, err1)) { - t.Errorf("error is not match: a: %v b: %v ", err2, err1) - } -} - -func TestErrorAs(t *testing.T) { - err1 := &StatusError{Code: 1} - err2 := fmt.Errorf("wrap : %w", err1) - - err3 := new(StatusError) - if !errors.As(err2, &err3) { - t.Errorf("error is not match: %v", err2) + if err3.Metadata["foo"] != "bar" { + t.Error("not expected metadata") } } diff --git a/errors/types.go b/errors/types.go new file mode 100644 index 000000000..0a7d2d96f --- /dev/null +++ b/errors/types.go @@ -0,0 +1,80 @@ +package errors + +import "net/http" + +// BadRequest new BadRequest error that is mapped to a 400 response. +func BadRequest(reason, message string) *Error { + return New(http.StatusBadRequest, reason, message) +} + +// IsBadRequest determines if err is an error which indicates a BadRequest error. +// It supports wrapped errors. +func IsBadRequest(err error) bool { + return Code(err) == http.StatusBadRequest +} + +// Unauthorized new Unauthorized error that is mapped to a 401 response. +func Unauthorized(reason, message string) *Error { + return New(http.StatusUnauthorized, reason, message) +} + +// IsUnauthorized determines if err is an error which indicates a Unauthorized error. +// It supports wrapped errors. +func IsUnauthorized(err error) bool { + return Code(err) == http.StatusUnauthorized +} + +// Forbidden new Forbidden error that is mapped to a 403 response. +func Forbidden(reason, message string) *Error { + return New(http.StatusForbidden, reason, message) +} + +// IsForbidden determines if err is an error which indicates a Forbidden error. +// It supports wrapped errors. +func IsForbidden(err error) bool { + return Code(err) == http.StatusForbidden +} + +// NotFound new NotFound error that is mapped to a 404 response. +func NotFound(reason, message string) *Error { + return New(http.StatusNotFound, reason, message) +} + +// IsNotFound determines if err is an error which indicates an NotFound error. +// It supports wrapped errors. +func IsNotFound(err error) bool { + return Code(err) == http.StatusNotFound +} + +// Conflict new Conflict error that is mapped to a 409 response. +func Conflict(reason, message string) *Error { + return New(http.StatusConflict, reason, message) +} + +// IsConflict determines if err is an error which indicates a Conflict error. +// It supports wrapped errors. +func IsConflict(err error) bool { + return Code(err) == http.StatusConflict +} + +// InternalServer new InternalServer error that is mapped to a 500 response. +func InternalServer(reason, message string) *Error { + return New(http.StatusInternalServerError, reason, message) +} + +// IsInternalServer determines if err is an error which indicates an InternalServer error. +// It supports wrapped errors. +func IsInternalServer(err error) bool { + return Code(err) == http.StatusInternalServerError +} + +// ServiceUnavailable new ServiceUnavailable error that is mapped to a HTTP 503 response. +func ServiceUnavailable(reason, message string) *Error { + return New(http.StatusServiceUnavailable, reason, message) +} + +// IsServiceUnavailable determines if err is an error which indicates a ServiceUnavailable error. +// It supports wrapped errors. +func IsServiceUnavailable(err error) bool { + return Code(err) == http.StatusServiceUnavailable +} diff --git a/errors/types_test.go b/errors/types_test.go new file mode 100644 index 000000000..cc9dc5b7d --- /dev/null +++ b/errors/types_test.go @@ -0,0 +1,32 @@ +package errors + +import "testing" + +func TestTypes(t *testing.T) { + var ( + input = []*Error{ + BadRequest("reason_400", "message_400"), + Unauthorized("reason_401", "message_401"), + Forbidden("reason_403", "message_403"), + NotFound("reason_404", "message_404"), + Conflict("reason_409", "message_409"), + InternalServer("reason_500", "message_500"), + ServiceUnavailable("reason_503", "message_503"), + } + output = []func(error) bool{ + IsBadRequest, + IsUnauthorized, + IsForbidden, + IsNotFound, + IsConflict, + IsInternalServer, + IsServiceUnavailable, + } + ) + + for i, in := range input { + if !output[i](in) { + t.Errorf("not expect: %v", in) + } + } +} diff --git a/middleware/metrics/metrics.go b/middleware/metrics/metrics.go index d27fe7bb0..bc99dc5a1 100644 --- a/middleware/metrics/metrics.go +++ b/middleware/metrics/metrics.go @@ -48,7 +48,7 @@ func Server(opts ...Option) middleware.Middleware { var ( method string path string - code int32 + code int ) if info, ok := grpc.FromServerContext(ctx); ok { method = "POST" @@ -90,7 +90,7 @@ func Client(opts ...Option) middleware.Middleware { var ( method string path string - code int32 + code int ) if info, ok := grpc.FromClientContext(ctx); ok { method = "POST" diff --git a/middleware/recovery/recovery.go b/middleware/recovery/recovery.go index f86e3fa53..7d8e77b56 100644 --- a/middleware/recovery/recovery.go +++ b/middleware/recovery/recovery.go @@ -2,6 +2,7 @@ package recovery import ( "context" + "fmt" "runtime" "github.com/go-kratos/kratos/v2/errors" @@ -39,7 +40,7 @@ func Recovery(opts ...Option) middleware.Middleware { options := options{ logger: log.DefaultLogger, handler: func(ctx context.Context, req, err interface{}) error { - return errors.Unknown("Unknown", "panic triggered: %v", err) + return errors.InternalServer("recovery", fmt.Sprintf("panic triggered: %v", err)) }, } for _, o := range opts { diff --git a/middleware/status/status.go b/middleware/status/status.go index d01121ffc..a1e6db852 100644 --- a/middleware/status/status.go +++ b/middleware/status/status.go @@ -2,28 +2,38 @@ package status import ( "context" + "net/http" "github.com/go-kratos/kratos/v2/errors" "github.com/go-kratos/kratos/v2/middleware" //lint:ignore SA1019 grpc "github.com/golang/protobuf/proto" - "github.com/golang/protobuf/ptypes" "google.golang.org/genproto/googleapis/rpc/errdetails" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) +type domainKey struct{} + // HandlerFunc is middleware error handler. -type HandlerFunc func(error) error +type HandlerFunc func(context.Context, error) error // Option is recovery option. type Option func(*options) type options struct { + domain string handler HandlerFunc } +// WithDomain with service domain. +func WithDomain(domain string) Option { + return func(o *options) { + o.domain = domain + } +} + // WithHandler with status handler. func WithHandler(h HandlerFunc) Option { return func(o *options) { @@ -34,7 +44,7 @@ func WithHandler(h HandlerFunc) Option { // Server is an error middleware. func Server(opts ...Option) middleware.Middleware { options := options{ - handler: errorEncode, + handler: encodeErr, } for _, o := range opts { o(&options) @@ -43,7 +53,8 @@ func Server(opts ...Option) middleware.Middleware { return func(ctx context.Context, req interface{}) (interface{}, error) { reply, err := handler(ctx, req) if err != nil { - return nil, options.handler(err) + ctx = context.WithValue(ctx, domainKey{}, options.domain) + return nil, options.handler(ctx, err) } return reply, nil } @@ -53,7 +64,7 @@ func Server(opts ...Option) middleware.Middleware { // Client is an error middleware. func Client(opts ...Option) middleware.Middleware { options := options{ - handler: errorDecode, + handler: decodeErr, } for _, o := range opts { o(&options) @@ -62,35 +73,26 @@ func Client(opts ...Option) middleware.Middleware { return func(ctx context.Context, req interface{}) (interface{}, error) { reply, err := handler(ctx, req) if err != nil { - return nil, options.handler(err) + return nil, options.handler(ctx, err) } return reply, nil } } } -func errorEncode(err error) error { - se, ok := errors.FromError(err) - if !ok { - se = &errors.StatusError{ - Code: 2, - Message: err.Error(), - } +func encodeErr(ctx context.Context, err error) error { + se := errors.FromError(err) + if se.Domain == "" { + se.Domain, _ = ctx.Value(domainKey{}).(string) } - gs := status.Newf(codes.Code(se.Code), "%s: %s", se.Reason, se.Message) + gs := status.Newf(httpToGRPCCode(se.Code), "%s: %s", se.Reason, se.Message) details := []proto.Message{ &errdetails.ErrorInfo{ + Domain: se.Domain, Reason: se.Reason, - Metadata: map[string]string{"message": se.Message}, + Metadata: se.Metadata, }, } - for _, any := range se.Details { - detail := &ptypes.DynamicAny{} - if err := ptypes.UnmarshalAny(any, detail); err != nil { - continue - } - details = append(details, detail.Message) - } gs, err = gs.WithDetails(details...) if err != nil { return err @@ -98,19 +100,60 @@ func errorEncode(err error) error { return gs.Err() } -func errorDecode(err error) error { +func decodeErr(ctx context.Context, err error) error { gs := status.Convert(err) - se := &errors.StatusError{ - Code: int32(gs.Code()), - Details: gs.Proto().Details, + se := &errors.Error{ + Code: grpcToHTTPCode(gs.Code()), + Message: gs.Message(), } for _, detail := range gs.Details() { switch d := detail.(type) { case *errdetails.ErrorInfo: + se.Domain = d.Domain se.Reason = d.Reason - se.Message = d.Metadata["message"] + se.Metadata = d.Metadata return se } } return se } + +func httpToGRPCCode(code int) codes.Code { + switch code { + case http.StatusBadRequest: + return codes.InvalidArgument + case http.StatusUnauthorized: + return codes.Unauthenticated + case http.StatusForbidden: + return codes.PermissionDenied + case http.StatusNotFound: + return codes.NotFound + case http.StatusConflict: + return codes.Aborted + case http.StatusInternalServerError: + return codes.Internal + case http.StatusServiceUnavailable: + return codes.Unavailable + } + return codes.Unknown +} + +func grpcToHTTPCode(code codes.Code) int { + switch code { + case codes.InvalidArgument: + return http.StatusBadRequest + case codes.Unauthenticated: + return http.StatusUnauthorized + case codes.PermissionDenied: + return http.StatusForbidden + case codes.NotFound: + return http.StatusNotFound + case codes.Aborted: + return http.StatusConflict + case codes.Internal: + return http.StatusInternalServerError + case codes.Unavailable: + return http.StatusServiceUnavailable + } + return http.StatusInternalServerError +} diff --git a/middleware/status/status_test.go b/middleware/status/status_test.go index f6636bba9..55a5d8dc4 100644 --- a/middleware/status/status_test.go +++ b/middleware/status/status_test.go @@ -1,6 +1,7 @@ package status import ( + "context" "testing" "github.com/go-kratos/kratos/v2/errors" @@ -9,13 +10,13 @@ import ( ) func TestErrEncoder(t *testing.T) { - err := errors.InvalidArgument("InvalidArgument", "format") - en := errorEncode(err) + err := errors.BadRequest("InvalidArgument", "format") + en := encodeErr(context.Background(), err) if code := status.Code(en); code != codes.InvalidArgument { t.Errorf("expected %d got %d", codes.InvalidArgument, code) } - de := errorDecode(en) - if !errors.IsInvalidArgument(de) { + de := decodeErr(context.Background(), en) + if !errors.IsBadRequest(de) { t.Errorf("expected %v got %v", err, de) } } diff --git a/middleware/validate/validate.go b/middleware/validate/validate.go index 884bb3963..5d2b3d587 100644 --- a/middleware/validate/validate.go +++ b/middleware/validate/validate.go @@ -17,7 +17,7 @@ func Validator() middleware.Middleware { return func(ctx context.Context, req interface{}) (reply interface{}, err error) { if v, ok := req.(validator); ok { if err := v.Validate(); err != nil { - return nil, errors.InvalidArgument("Validator", err.Error()) + return nil, errors.BadRequest("validator", err.Error()) } } return handler(ctx, req) diff --git a/transport/http/client.go b/transport/http/client.go index 38e0fb4af..95b4e1bb9 100644 --- a/transport/http/client.go +++ b/transport/http/client.go @@ -117,7 +117,7 @@ func Do(client *http.Client, req *http.Request, target interface{}) error { } defer res.Body.Close() if res.StatusCode < 200 || res.StatusCode > 299 { - se := &errors.StatusError{Code: 2} + se := &errors.Error{Code: 500} if err := decodeResponse(res, se); err != nil { return err } diff --git a/transport/http/handle.go b/transport/http/handle.go index b96301469..1bce7e9a6 100644 --- a/transport/http/handle.go +++ b/transport/http/handle.go @@ -108,17 +108,11 @@ func encodeResponse(w http.ResponseWriter, r *http.Request, v interface{}) error // encodeError encodes the error to the HTTP response. func encodeError(w http.ResponseWriter, r *http.Request, err error) { - se, ok := errors.FromError(err) - if !ok { - se = &errors.StatusError{ - Code: 2, - Message: err.Error(), - } - } + se := errors.FromError(err) codec := codecForRequest(r) data, _ := codec.Marshal(se) w.Header().Set(contentTypeHeader, contentType(codec.Name())) - w.WriteHeader(se.HTTPStatus()) + w.WriteHeader(se.Code) _, _ = w.Write(data) }