diff --git a/dax/controller/client/client.go b/dax/controller/client/client.go index 055007a27..4a672bc39 100644 --- a/dax/controller/client/client.go +++ b/dax/controller/client/client.go @@ -159,7 +159,7 @@ func (c *Client) DatabaseByName(ctx context.Context, orgID dax.OrganizationID, n if resp.StatusCode != http.StatusOK { b, _ := io.ReadAll(resp.Body) - return nil, errors.Errorf("status code: %d: %s", resp.StatusCode, b) + return nil, errors.Wrapf(errors.UnmarshalJSON(b), "status code: %d", resp.StatusCode) } var qdb *dax.QualifiedDatabase diff --git a/dax/controller/http/handler.go b/dax/controller/http/handler.go index cb07735bd..c9567ea15 100644 --- a/dax/controller/http/handler.go +++ b/dax/controller/http/handler.go @@ -6,6 +6,7 @@ import ( "github.com/featurebasedb/featurebase/v3/dax" "github.com/featurebasedb/featurebase/v3/dax/controller" + "github.com/featurebasedb/featurebase/v3/errors" "github.com/gorilla/mux" ) @@ -146,7 +147,7 @@ func (s *server) postDatabaseByName(w http.ResponseWriter, r *http.Request) { } resp, err := s.controller.DatabaseByName(ctx, req.OrganizationID, req.Name) if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + http.Error(w, errors.MarshalJSON(err), http.StatusBadRequest) return } diff --git a/errors/errors.go b/errors/errors.go index 230788aaf..b49d96256 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -3,6 +3,8 @@ package errors import ( + "encoding/json" + "github.com/pkg/errors" ) @@ -12,8 +14,8 @@ type Code string func New(code Code, message string) error { return errors.WithStack(codedError{ - code: code, - message: message, + Code: code, + Message: message, }) } @@ -33,7 +35,7 @@ func Errorf(format string, args ...interface{}) error { // an error Code instead of an error. func Is(err error, target Code) bool { match := codedError{ - code: target, + Code: target, } return errors.Is(err, match) } @@ -65,12 +67,13 @@ func Wrapf(err error, fmt string, args ...interface{}) error { // codedError is the fundamental type used by this package to provide coded // errors. type codedError struct { - code Code - message string + Code Code `json:"code"` + Message string `json:"message"` + Wrapped string `json:"wrapped,omitempty"` } func (ce codedError) Error() string { - return ce.message + return ce.Message } // func (ce codedError) As(target interface{}) bool { @@ -78,7 +81,7 @@ func (ce codedError) Error() string { // } func (ce codedError) Is(err error) bool { - if e, ok := err.(codedError); ok && ce.code == e.code { + if e, ok := err.(codedError); ok && ce.Code == e.Code { return true } return false @@ -87,3 +90,50 @@ func (ce codedError) Is(err error) bool { const ( ErrUncoded Code = "Uncoded" ) + +// MarshalJSON returns the provided error as a json object (as a string) +// representing a codedError. If err is not already a codedError, the json +// object will still represent a codedError but its `code` value will be empty. +// Note: an empty code here is intentional and is different from code +// `errors.Uncoded` which is a valid code; it just means the developer returned +// a codedError but didn't bother to choose (or create) a useful error code. +func MarshalJSON(err error) string { + cause := Cause(err) + + var out *codedError + + switch v := cause.(type) { + case codedError: + v.Wrapped = err.Error() + out = &v + default: + out = &codedError{ + Message: cause.Error(), + Wrapped: err.Error(), + } + } + + // Marshal the codedError to json as output. + j, jerr := json.Marshal(out) + if jerr != nil { + return out.Error() + } + + return string(j) + +} + +// UnmarshalJSON converts the byte slice into a codedError. If the bytes can't +// unmarshal to a codedError, a normal error will be returned containing the +// string value of the byte slice. +func UnmarshalJSON(b []byte) error { + if len(b) == 0 { + return nil + } + + out := &codedError{} + if err := json.Unmarshal(b, out); err != nil { + return errors.New(string(b)) + } + return out +}