Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide error codes for enhancing error handling from clients #927

Merged
merged 3 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions api/converter/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package converter

import (
"errors"

"connectrpc.com/connect"
"google.golang.org/genproto/googleapis/rpc/errdetails"
)

// ErrorCodeOf returns the error code of the given error.
func ErrorCodeOf(err error) string {
var connectErr *connect.Error
if !errors.As(err, &connectErr) {
return ""
}
for _, detail := range connectErr.Details() {
msg, valueErr := detail.Value()
if valueErr != nil {
continue
}

if errorInfo, ok := msg.(*errdetails.ErrorInfo); ok {
return errorInfo.GetMetadata()["code"]
}
}
return ""
}
143 changes: 114 additions & 29 deletions server/rpc/connecthelper/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ package connecthelper
import (
"context"
"errors"
"fmt"

"connectrpc.com/connect"
"google.golang.org/genproto/googleapis/rpc/errdetails"
Expand All @@ -37,8 +36,8 @@ import (
"github.com/yorkie-team/yorkie/server/rpc/auth"
)

// errorToCode maps an error to connectRPC status code.
var errorToCode = map[error]connect.Code{
// errorToConnectCode maps an error to connectRPC status code.
var errorToConnectCode = map[error]connect.Code{
// InvalidArgument means the request is malformed.
converter.ErrPackRequired: connect.CodeInvalidArgument,
converter.ErrCheckpointRequired: connect.CodeInvalidArgument,
Expand Down Expand Up @@ -88,9 +87,110 @@ var errorToCode = map[error]connect.Code{
context.Canceled: connect.CodeCanceled,
}

func detailsFromError(err error) (*errdetails.BadRequest, bool) {
invalidFieldsError, ok := err.(*validation.StructError)
// errorToCode maps an error to a string representation of the error.
// TODO(hackerwins): We need to add codes by hand for each error. It would be
// better to generate this map automatically.
var errorToCode = map[error]string{
converter.ErrPackRequired: "ErrPackRequired",
converter.ErrCheckpointRequired: "ErrCheckpointRequired",
time.ErrInvalidHexString: "ErrInvalidHexString",
time.ErrInvalidActorID: "ErrInvalidActorID",
types.ErrInvalidID: "ErrInvalidID",
clients.ErrInvalidClientID: "ErrInvalidClientID",
clients.ErrInvalidClientKey: "ErrInvalidClientKey",
key.ErrInvalidKey: "ErrInvalidKey",
types.ErrEmptyProjectFields: "ErrEmptyProjectFields",

database.ErrProjectNotFound: "ErrProjectNotFound",
database.ErrClientNotFound: "ErrClientNotFound",
database.ErrDocumentNotFound: "ErrDocumentNotFound",
database.ErrUserNotFound: "ErrUserNotFound",

database.ErrProjectAlreadyExists: "ErrProjectAlreadyExists",
database.ErrProjectNameAlreadyExists: "ErrProjectNameAlreadyExists",
database.ErrUserAlreadyExists: "ErrUserAlreadyExists",

database.ErrClientNotActivated: "ErrClientNotActivated",
database.ErrDocumentNotAttached: "ErrDocumentNotAttached",
database.ErrDocumentAlreadyAttached: "ErrDocumentAlreadyAttached",
database.ErrDocumentAlreadyDetached: "ErrDocumentAlreadyDetached",
documents.ErrDocumentAttached: "ErrDocumentAttached",
packs.ErrInvalidServerSeq: "ErrInvalidServerSeq",
database.ErrConflictOnUpdate: "ErrConflictOnUpdate",

converter.ErrUnsupportedOperation: "ErrUnsupportedOperation",
converter.ErrUnsupportedElement: "ErrUnsupportedElement",
converter.ErrUnsupportedEventType: "ErrUnsupportedEventType",
converter.ErrUnsupportedValueType: "ErrUnsupportedValueType",
converter.ErrUnsupportedCounterType: "ErrUnsupportedCounterType",

auth.ErrNotAllowed: "ErrNotAllowed",
auth.ErrUnexpectedStatusCode: "ErrUnexpectedStatusCode",
auth.ErrWebhookTimeout: "ErrWebhookTimeout",
database.ErrMismatchedPassword: "ErrMismatchedPassword",
}

// CodeOf returns the string representation of the given error.
func CodeOf(err error) string {
cause := err
for errors.Unwrap(cause) != nil {
cause = errors.Unwrap(cause)
}

if code, ok := errorToCode[cause]; ok {
return code
}

return ""
}

// errorToConnectError returns connect.Error from the given error.
func errorToConnectError(err error) (*connect.Error, bool) {
cause := err
for errors.Unwrap(cause) != nil {
cause = errors.Unwrap(cause)
}

connectCode, ok := errorToConnectCode[cause]
if !ok {
return nil, false
}

connectErr := connect.NewError(connectCode, err)
if code, ok := errorToCode[cause]; ok {
errorInfo := &errdetails.ErrorInfo{
Metadata: map[string]string{"code": code},
}
if detail, detailErr := connect.NewErrorDetail(errorInfo); detailErr == nil {
connectErr.AddDetail(detail)
}
}

return connectErr, true
}

// structErrorToConnectError returns connect.Error from the given struct error.
func structErrorToConnectError(err error) (*connect.Error, bool) {
var invalidFieldsError *validation.StructError
if !errors.As(err, &invalidFieldsError) {
return nil, false
}

connectErr := connect.NewError(connect.CodeInvalidArgument, err)
badRequest, ok := badRequestFromError(err)
if !ok {
return connectErr, true
}
if detail, err := connect.NewErrorDetail(badRequest); err == nil {
connectErr.AddDetail(detail)
}

return connectErr, true
}

func badRequestFromError(err error) (*errdetails.BadRequest, bool) {
var invalidFieldsError *validation.StructError
if !errors.As(err, &invalidFieldsError) {
return nil, false
}

Expand All @@ -107,38 +207,23 @@ func detailsFromError(err error) (*errdetails.BadRequest, bool) {
return br, true
}

// ToStatusError returns a connect.Error from the given logic error. If an error
// ToStatusError returns connect.Error from the given logic error. If an error
// occurs while executing logic in API handler, connectRPC connect.error should be
// returned so that the client can know more about the status of the request.
func ToStatusError(err error) error {
cause := err
for errors.Unwrap(cause) != nil {
cause = errors.Unwrap(cause)
}
if code, ok := errorToCode[cause]; ok {
return connect.NewError(code, err)
if err == nil {
return nil
}

// NOTE(hackerwins): InvalidFieldsError has details of invalid fields in
// the error message.
var invalidFieldsError *validation.StructError
if errors.As(err, &invalidFieldsError) {
st := connect.NewError(connect.CodeInvalidArgument, err)
details, ok := detailsFromError(err)
if !ok {
return st
}
if detail, err := connect.NewErrorDetail(details); err == nil {
st.AddDetail(detail)
}
return st
if err, ok := errorToConnectError(err); ok {
return err
}

if err := connect.NewError(connect.CodeInternal, err); err != nil {
return fmt.Errorf("create status error: %w", err)
if err, ok := structErrorToConnectError(err); ok {
return err
}

return nil
return connect.NewError(connect.CodeInternal, err)
}

// ToRPCCodeString returns a string representation of the given error.
Expand All @@ -151,7 +236,7 @@ func ToRPCCodeString(err error) string {
for errors.Unwrap(cause) != nil {
cause = errors.Unwrap(cause)
}
if code, ok := errorToCode[cause]; ok {
if code, ok := errorToConnectCode[cause]; ok {
return code.String()
}

Expand Down
Loading
Loading