diff --git a/graphql/error.go b/graphql/error.go index a6a5c6ea483..fd20dc9c2c6 100644 --- a/graphql/error.go +++ b/graphql/error.go @@ -32,5 +32,10 @@ func MarshalError(err *errors.QueryError) Marshaler { errObj.Add("locations", locations) } + + if err.ExtraInfo != nil { + errObj.Add(`extraInfo`, MarshalJSON(err.ExtraInfo)) + } + return errObj } diff --git a/graphql/jsonw.go b/graphql/jsonw.go index ef9e69c7ab1..4ebeacad160 100644 --- a/graphql/jsonw.go +++ b/graphql/jsonw.go @@ -1,6 +1,7 @@ package graphql import ( + "encoding/json" "io" "strconv" ) @@ -81,3 +82,10 @@ func lit(b []byte) Marshaler { w.Write(b) }) } + +// MarshalJSON marshals an interface into JSON. +func MarshalJSON(i interface{}) Marshaler { + return WriterFunc(func(w io.Writer) { + json.NewEncoder(w).Encode(i) + }) +} diff --git a/handler/graphql.go b/handler/graphql.go index e2a6189ec8a..2606c31dcdd 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -23,7 +23,7 @@ type params struct { type Config struct { upgrader websocket.Upgrader recover graphql.RecoverFunc - formatError func(error) string + formatError errors.ErrorMessageFunc resolverHook graphql.ResolverMiddleware requestHook graphql.RequestMiddleware } @@ -42,7 +42,7 @@ func RecoverFunc(recover graphql.RecoverFunc) Option { } } -func FormatErrorFunc(f func(error) string) Option { +func FormatErrorFunc(f errors.ErrorMessageFunc) Option { return func(cfg *Config) { cfg.formatError = f } diff --git a/neelance/errors/errors.go b/neelance/errors/errors.go index a442b8b96d3..73b706f6c61 100644 --- a/neelance/errors/errors.go +++ b/neelance/errors/errors.go @@ -10,6 +10,7 @@ type QueryError struct { Path []interface{} `json:"path,omitempty"` Rule string `json:"-"` ResolverError error `json:"-"` + ExtraInfo interface{} `json:"extraInfo,omitempty"` } type Location struct { @@ -29,10 +30,11 @@ func Errorf(format string, a ...interface{}) *QueryError { // WithMessagef is the same as Errorf, except it will store the err inside // the ResolverError field. -func WithMessagef(err error, format string, a ...interface{}) *QueryError { +func WithMessagef(err error, extra interface{}, format string, a ...interface{}) *QueryError { return &QueryError{ Message: fmt.Sprintf(format, a...), ResolverError: err, + ExtraInfo: extra, } } @@ -49,13 +51,23 @@ func (err *QueryError) Error() string { var _ error = &QueryError{} +// FormattedError a formatted error which includes the error message and extra information +// which is JSON encoded and passed with the error. +type FormattedError struct { + Message string + Extra interface{} +} + +// ErrorMessageFunc a func which given an error returns a formatted error. +type ErrorMessageFunc func(err error) FormattedError + type Builder struct { Errors []*QueryError // ErrorMessageFn will be used to generate the error // message from errors given to Error(). // // If ErrorMessageFn is nil, err.Error() will be used. - ErrorMessageFn func(error) string + ErrorMessageFn ErrorMessageFunc } func (c *Builder) Errorf(format string, args ...interface{}) { @@ -63,11 +75,11 @@ func (c *Builder) Errorf(format string, args ...interface{}) { } func (c *Builder) Error(err error) { - var gqlErrMessage string + fErr := FormattedError{Message: err.Error()} + if c.ErrorMessageFn != nil { - gqlErrMessage = c.ErrorMessageFn(err) - } else { - gqlErrMessage = err.Error() + fErr = c.ErrorMessageFn(err) } - c.Errors = append(c.Errors, WithMessagef(err, gqlErrMessage)) + + c.Errors = append(c.Errors, WithMessagef(err, fErr.Extra, fErr.Message)) } diff --git a/neelance/errors/errors_test.go b/neelance/errors/errors_test.go index 698de10ef98..84f3f5fe602 100644 --- a/neelance/errors/errors_test.go +++ b/neelance/errors/errors_test.go @@ -55,9 +55,10 @@ func (err *publicErr) PublicError() string { return err.public } -func convertErr(err error) string { +func convertErr(err error) FormattedError { if errConv, ok := err.(*publicErr); ok { - return errConv.public + return FormattedError{Message: errConv.public} } - return err.Error() + + return FormattedError{Message: err.Error()} } diff --git a/test/resolvers_test.go b/test/resolvers_test.go index edf119e2261..eb244d85750 100644 --- a/test/resolvers_test.go +++ b/test/resolvers_test.go @@ -18,6 +18,8 @@ import ( "github.com/vektah/gqlgen/test/models" ) +type fErr = gqlerrors.FormattedError + func TestCompiles(t *testing.T) {} func TestErrorConverter(t *testing.T) { @@ -28,11 +30,11 @@ func TestErrorConverter(t *testing.T) { require.Nil(t, errs) t.Run("with", func(t *testing.T) { - testConvErr := func(e error) string { + testConvErr := func(e error) fErr { if _, ok := errors.Cause(e).(*specialErr); ok { - return "override special error message" + return fErr{Message: "override special error message"} } - return e.Error() + return fErr{Message: e.Error()} } t.Run("special error", func(t *testing.T) { resolvers.nestedOutputsErr = &specialErr{} @@ -68,7 +70,7 @@ func TestErrorConverter(t *testing.T) { }) } -func mkctx(doc *query.Document, errFn func(e error) string) context.Context { +func mkctx(doc *query.Document, errFn gqlerrors.ErrorMessageFunc) context.Context { return graphql.WithRequestContext(context.Background(), &graphql.RequestContext{ Doc: doc, ResolverMiddleware: func(ctx context.Context, next graphql.Resolver) (res interface{}, err error) {