diff --git a/.gitattributes b/.gitattributes index f6ffcb7da1e..df1ea743119 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ /codegen/templates/data.go linguist-generated /example/dataloader/*_gen.go linguist-generated +generated.go linguist-generated diff --git a/README.md b/README.md index 034cbb36c4a..05dfb9715b6 100644 --- a/README.md +++ b/README.md @@ -25,3 +25,4 @@ See the [docs](https://gqlgen.com/) for a getting started guide. | Hooks for error logging | :+1: | :no_entry: | :no_entry: | :no_entry: | | Dataloading | :+1: | :+1: | :no_entry: | :warning: | | Concurrency | :+1: | :+1: | :no_entry: [pr](https://github.com/graphql-go/graphql/pull/132) | :+1: | +| Custom errors & error.path | :+1: | :no_entry: [is](https://github.com/graphql-go/graphql/issues/259) | :no_entry: | :no_entry: | diff --git a/client/client.go b/client/client.go index 0b83aad9898..169a1327ba5 100644 --- a/client/client.go +++ b/client/client.go @@ -9,7 +9,6 @@ import ( "net/http" "github.com/mitchellh/mapstructure" - "github.com/vektah/gqlgen/neelance/errors" ) // Client for graphql requests @@ -101,23 +100,28 @@ func (p *Client) Post(query string, response interface{}, options ...Option) err // decode it into map string first, let mapstructure do the final decode // because it can be much stricter about unknown fields. - respDataRaw := map[string]interface{}{} + respDataRaw := struct { + Data interface{} + Errors json.RawMessage + }{} err = json.Unmarshal(responseBody, &respDataRaw) if err != nil { return fmt.Errorf("decode: %s", err.Error()) } - if respDataRaw["errors"] != nil { - var errs []*errors.QueryError - if err := unpack(respDataRaw["errors"], &errs); err != nil { - return err - } - if len(errs) > 0 { - return fmt.Errorf("errors: %s", errs) - } + if respDataRaw.Errors != nil { + return RawJsonError{respDataRaw.Errors} } - return unpack(respDataRaw["data"], response) + return unpack(respDataRaw.Data, response) +} + +type RawJsonError struct { + json.RawMessage +} + +func (r RawJsonError) Error() string { + return string(r.RawMessage) } func unpack(data interface{}, into interface{}) error { diff --git a/codegen/codegen.go b/codegen/codegen.go index 7fd9ecc6a52..26a1eefb195 100644 --- a/codegen/codegen.go +++ b/codegen/codegen.go @@ -168,7 +168,7 @@ func write(filename string, b []byte) error { formatted, err := gofmt(filename, b) if err != nil { - fmt.Fprintf(os.Stderr, "gofmt failed: %s", err.Error()) + fmt.Fprintf(os.Stderr, "gofmt failed: %s\n", err.Error()) formatted = b } diff --git a/codegen/object.go b/codegen/object.go index 3fd1f63ddad..5a4de009a6c 100644 --- a/codegen/object.go +++ b/codegen/object.go @@ -82,7 +82,7 @@ func (f *Field) CallArgs() string { var args []string if f.GoMethodName == "" { - args = append(args, "rctx") + args = append(args, "ctx") if !f.Object.Root { args = append(args, "obj") @@ -115,7 +115,12 @@ func (f *Field) doWriteJson(val string, remainingMods []string, isPtr bool, dept return tpl(`{{.arr}} := graphql.Array{} for {{.index}} := range {{.val}} { - {{.arr}} = append({{.arr}}, func() graphql.Marshaler { {{ .next }} }()) + {{.arr}} = append({{.arr}}, func() graphql.Marshaler { + rctx := graphql.GetResolverContext(ctx) + rctx.PushIndex({{.index}}) + defer rctx.Pop() + {{ .next }} + }()) } return {{.arr}}`, map[string]interface{}{ "val": val, diff --git a/codegen/templates/args.gotpl b/codegen/templates/args.gotpl index af0f5829b69..f53aceec1e5 100644 --- a/codegen/templates/args.gotpl +++ b/codegen/templates/args.gotpl @@ -5,7 +5,7 @@ var err error {{$arg.Unmarshal (print "arg" $i) "tmp" }} if err != nil { - ec.Error(err) + ec.Error(ctx, err) {{- if $arg.Object.Stream }} return nil {{- else }} @@ -17,7 +17,7 @@ var err error {{$arg.Unmarshal (print "arg" $i) "tmp" }} if err != nil { - ec.Error(err) + ec.Error(ctx, err) {{- if $arg.Object.Stream }} return nil {{- else }} diff --git a/codegen/templates/field.gotpl b/codegen/templates/field.gotpl index 4a816482219..87f9611c0c1 100644 --- a/codegen/templates/field.gotpl +++ b/codegen/templates/field.gotpl @@ -4,10 +4,10 @@ {{- if $object.Stream }} func (ec *executionContext) _{{$object.GQLType}}_{{$field.GQLName}}(ctx context.Context, field graphql.CollectedField) func() graphql.Marshaler { {{- template "args.gotpl" $field.Args }} - rctx := graphql.WithResolverContext(ctx, &graphql.ResolverContext{Field: field}) + ctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{Field: field}) results, err := ec.resolvers.{{ $object.GQLType }}_{{ $field.GQLName }}({{ $field.CallArgs }}) if err != nil { - ec.Error(err) + ec.Error(ctx, err) return nil } return func() graphql.Marshaler { @@ -25,14 +25,26 @@ {{- template "args.gotpl" $field.Args }} {{- if $field.IsConcurrent }} + ctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{ + Object: {{$object.GQLType|quote}}, + Args: {{if $field.Args }}args{{else}}nil{{end}}, + Field: field, + }) return graphql.Defer(func() (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { userErr := ec.Recover(ctx, r) - ec.Error(userErr) + ec.Error(ctx, userErr) ret = graphql.Null } }() + {{ else }} + rctx := graphql.GetResolverContext(ctx) + rctx.Object = {{$object.GQLType|quote}} + rctx.Args = {{if $field.Args }}args{{else}}nil{{end}} + rctx.Field = field + rctx.PushField(field.Alias) + defer rctx.Pop() {{- end }} {{- if $field.GoVarName }} @@ -43,21 +55,17 @@ {{- else }} res, err := {{$field.GoMethodName}}({{ $field.CallArgs }}) if err != nil { - ec.Error(err) + ec.Error(ctx, err) return graphql.Null } {{- end }} {{- else }} - rctx := graphql.WithResolverContext(ctx, &graphql.ResolverContext{ - Object: {{$object.GQLType|quote}}, - Args: {{if $field.Args }}args{{else}}nil{{end}}, - Field: field, - }) - resTmp, err := ec.ResolverMiddleware(rctx, func(rctx context.Context) (interface{}, error) { + + resTmp, err := ec.ResolverMiddleware(ctx, func(ctx context.Context) (interface{}, error) { return ec.resolvers.{{ $object.GQLType }}_{{ $field.GQLName }}({{ $field.CallArgs }}) }) if err != nil { - ec.Error(err) + ec.Error(ctx, err) return graphql.Null } if resTmp == nil { diff --git a/codegen/templates/generated.gotpl b/codegen/templates/generated.gotpl index 5ae2120cd7b..9e579eae3c9 100644 --- a/codegen/templates/generated.gotpl +++ b/codegen/templates/generated.gotpl @@ -44,7 +44,7 @@ func (e *executableSchema) Query(ctx context.Context, op *query.Operation) *grap Errors: ec.Errors, } {{- else }} - return &graphql.Response{Errors: []*errors.QueryError{ {Message: "queries are not supported"} }} + return graphql.ErrorResponse(ctx, "queries are not supported") {{- end }} } @@ -64,7 +64,7 @@ func (e *executableSchema) Mutation(ctx context.Context, op *query.Operation) *g Errors: ec.Errors, } {{- else }} - return &graphql.Response{Errors: []*errors.QueryError{ {Message: "mutations are not supported"} }} + return graphql.ErrorResponse(ctx, "mutations are not supported") {{- end }} } @@ -96,7 +96,7 @@ func (e *executableSchema) Subscription(ctx context.Context, op *query.Operation } } {{- else }} - return graphql.OneShot(&graphql.Response{Errors: []*errors.QueryError{ {Message: "subscriptions are not supported"} }}) + return graphql.OneShot(graphql.ErrorResponse(ctx, "subscriptions are not supported")) {{- end }} } diff --git a/codegen/templates/object.gotpl b/codegen/templates/object.gotpl index f8c3daea5da..b531d5fe6d0 100644 --- a/codegen/templates/object.gotpl +++ b/codegen/templates/object.gotpl @@ -6,9 +6,11 @@ var {{ $object.GQLType|lcFirst}}Implementors = {{$object.Implementors}} {{- if .Stream }} func (ec *executionContext) _{{$object.GQLType}}(ctx context.Context, sel []query.Selection) func() graphql.Marshaler { fields := graphql.CollectFields(ec.Doc, sel, {{$object.GQLType|lcFirst}}Implementors, ec.Variables) - + ctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{ + Object: {{$object.GQLType|quote}}, + }) if len(fields) != 1 { - ec.Errorf("must subscribe to exactly one stream") + ec.Errorf(ctx, "must subscribe to exactly one stream") return nil } @@ -24,6 +26,11 @@ func (ec *executionContext) _{{$object.GQLType}}(ctx context.Context, sel []quer {{- else }} func (ec *executionContext) _{{$object.GQLType}}(ctx context.Context, sel []query.Selection{{if not $object.Root}}, obj *{{$object.FullName}} {{end}}) graphql.Marshaler { fields := graphql.CollectFields(ec.Doc, sel, {{$object.GQLType|lcFirst}}Implementors, ec.Variables) + {{if $object.Root}} + ctx = graphql.WithResolverContext(ctx, &graphql.ResolverContext{ + Object: {{$object.GQLType|quote}}, + }) + {{end}} out := graphql.NewOrderedMap(len(fields)) for i, field := range fields { out.Keys[i] = field.Alias diff --git a/example/scalars/scalar_test.go b/example/scalars/scalar_test.go index 0899fde5f1a..34416ad92cf 100644 --- a/example/scalars/scalar_test.go +++ b/example/scalars/scalar_test.go @@ -59,7 +59,7 @@ func TestScalars(t *testing.T) { var resp struct{ Search []RawUser } err := c.Post(`{ search(input:{createdAfter:"2014"}) { id } }`, &resp) - require.EqualError(t, err, "errors: [graphql: time should be a unix timestamp]") + require.EqualError(t, err, `[{"message":"time should be a unix timestamp"}]`) }) t.Run("scalar resolver methods", func(t *testing.T) { diff --git a/example/starwars/starwars_test.go b/example/starwars/starwars_test.go index bc33edc214d..53cc7b32dd6 100644 --- a/example/starwars/starwars_test.go +++ b/example/starwars/starwars_test.go @@ -215,7 +215,7 @@ func TestStarwars(t *testing.T) { } }`, &resp, client.Var("episode", "INVALID")) - require.EqualError(t, err, "errors: [graphql: INVALID is not a valid Episode]") + require.EqualError(t, err, `[{"message":"INVALID is not a valid Episode"}]`) }) t.Run("introspection", func(t *testing.T) { diff --git a/example/todo/server/server.go b/example/todo/server/server.go index 0c824d0e795..e44ec3f1b41 100644 --- a/example/todo/server/server.go +++ b/example/todo/server/server.go @@ -16,7 +16,8 @@ func main() { http.Handle("/query", handler.GraphQL( todo.MakeExecutableSchema(todo.New()), handler.RecoverFunc(func(ctx context.Context, err interface{}) error { - log.Printf("send this panic somewhere") + // send this panic somewhere + log.Print(err) debug.PrintStack() return errors.New("user message on panic") }), diff --git a/example/todo/todo_test.go b/example/todo/todo_test.go index c7b6529c5d5..95972268401 100644 --- a/example/todo/todo_test.go +++ b/example/todo/todo_test.go @@ -80,7 +80,7 @@ func TestTodo(t *testing.T) { } err := c.Post(`{ todo(id:666) { text } }`, &resp) - require.EqualError(t, err, "errors: [graphql: internal system error]") + require.EqualError(t, err, `[{"message":"internal system error","path":["todo"]}]`) }) t.Run("select all", func(t *testing.T) { diff --git a/graphql/context.go b/graphql/context.go index e5210a4e401..adfe91e41ec 100644 --- a/graphql/context.go +++ b/graphql/context.go @@ -3,7 +3,6 @@ package graphql import ( "context" - "github.com/vektah/gqlgen/neelance/errors" "github.com/vektah/gqlgen/neelance/query" ) @@ -12,7 +11,7 @@ type ResolverMiddleware func(ctx context.Context, next Resolver) (res interface{ type RequestMiddleware func(ctx context.Context, next func(ctx context.Context) []byte) []byte type RequestContext struct { - errors.Builder + ErrorBuilder RawQuery string Variables map[string]interface{} @@ -49,6 +48,20 @@ type ResolverContext struct { Args map[string]interface{} // The raw field Field CollectedField + // The path of fields to get to this resolver + Path []interface{} +} + +func (r *ResolverContext) PushField(alias string) { + r.Path = append(r.Path, alias) +} + +func (r *ResolverContext) PushIndex(index int) { + r.Path = append(r.Path, index) +} + +func (r *ResolverContext) Pop() { + r.Path = r.Path[0 : len(r.Path)-1] } func GetResolverContext(ctx context.Context) *ResolverContext { @@ -61,6 +74,14 @@ func GetResolverContext(ctx context.Context) *ResolverContext { } func WithResolverContext(ctx context.Context, rc *ResolverContext) context.Context { + parent := GetResolverContext(ctx) + rc.Path = nil + if parent != nil { + rc.Path = append(rc.Path, parent.Path...) + } + if rc.Field.Alias != "" { + rc.PushField(rc.Field.Alias) + } return context.WithValue(ctx, resolver, rc) } diff --git a/graphql/error.go b/graphql/error.go index a6a5c6ea483..aafd69beaf2 100644 --- a/graphql/error.go +++ b/graphql/error.go @@ -1,36 +1,49 @@ package graphql import ( - "github.com/vektah/gqlgen/neelance/errors" + "context" + "fmt" + "sync" ) -func MarshalErrors(errs []*errors.QueryError) Marshaler { - res := Array{} - for _, err := range errs { - res = append(res, MarshalError(err)) +type ErrorPresenterFunc func(context.Context, error) error + +func DefaultErrorPresenter(ctx context.Context, err error) error { + return &ResolverError{ + Message: err.Error(), + Path: GetResolverContext(ctx).Path, } - return res } -func MarshalError(err *errors.QueryError) Marshaler { - if err == nil { - return Null - } +// ResolverError is the default error type returned by ErrorPresenter. You can replace it with your own by returning +// something different from the ErrorPresenter +type ResolverError struct { + Message string `json:"message"` + Path []interface{} `json:"path,omitempty"` +} - errObj := &OrderedMap{} - errObj.Add("message", MarshalString(err.Message)) +func (r *ResolverError) Error() string { + return r.Message +} - if len(err.Locations) > 0 { - locations := Array{} - for _, location := range err.Locations { - locationObj := &OrderedMap{} - locationObj.Add("line", MarshalInt(location.Line)) - locationObj.Add("column", MarshalInt(location.Column)) +type ErrorBuilder struct { + Errors []error + // ErrorPresenter will be used to generate the error + // message from errors given to Error(). + ErrorPresenter ErrorPresenterFunc + mu sync.Mutex +} - locations = append(locations, locationObj) - } +func (c *ErrorBuilder) Errorf(ctx context.Context, format string, args ...interface{}) { + c.mu.Lock() + defer c.mu.Unlock() - errObj.Add("locations", locations) - } - return errObj + c.Errors = append(c.Errors, c.ErrorPresenter(ctx, fmt.Errorf(format, args...))) +} + +func (c *ErrorBuilder) Error(ctx context.Context, err error) { + c.mu.Lock() + defer c.mu.Unlock() + + c.Errors = append(c.Errors, c.ErrorPresenter(ctx, err)) } diff --git a/graphql/errors_test.go b/graphql/errors_test.go new file mode 100644 index 00000000000..87f3c1cb1f8 --- /dev/null +++ b/graphql/errors_test.go @@ -0,0 +1,50 @@ +package graphql + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBuilder_Error(t *testing.T) { + b := ErrorBuilder{ErrorPresenter: convertErr} + b.Error(context.Background(), &testErr{"err1"}) + b.Error(context.Background(), &publicErr{ + message: "err2", + public: "err2 public", + }) + + require.Len(t, b.Errors, 2) + assert.EqualError(t, b.Errors[0], "err1") + assert.EqualError(t, b.Errors[1], "err2 public") +} + +type testErr struct { + message string +} + +func (err *testErr) Error() string { + return err.message +} + +type publicErr struct { + message string + public string +} + +func (err *publicErr) Error() string { + return err.message +} + +func (err *publicErr) PublicError() string { + return err.public +} + +func convertErr(ctx context.Context, err error) error { + if errConv, ok := err.(*publicErr); ok { + return &ResolverError{Message: errConv.public} + } + return &ResolverError{Message: err.Error()} +} diff --git a/graphql/response.go b/graphql/response.go index c09d2f0ed7d..09d491b7c4a 100644 --- a/graphql/response.go +++ b/graphql/response.go @@ -1,12 +1,18 @@ package graphql import ( + "context" "encoding/json" - - "github.com/vektah/gqlgen/neelance/errors" + "fmt" ) type Response struct { - Data json.RawMessage `json:"data"` - Errors []*errors.QueryError `json:"errors,omitempty"` + Data json.RawMessage `json:"data"` + Errors []error `json:"errors,omitempty"` +} + +func ErrorResponse(ctx context.Context, messagef string, args ...interface{}) *Response { + return &Response{ + Errors: []error{&ResolverError{Message: fmt.Sprintf(messagef, args...)}}, + } } diff --git a/handler/graphql.go b/handler/graphql.go index e2a6189ec8a..ab794632020 100644 --- a/handler/graphql.go +++ b/handler/graphql.go @@ -21,11 +21,11 @@ type params struct { } type Config struct { - upgrader websocket.Upgrader - recover graphql.RecoverFunc - formatError func(error) string - resolverHook graphql.ResolverMiddleware - requestHook graphql.RequestMiddleware + upgrader websocket.Upgrader + recover graphql.RecoverFunc + errorPresenter graphql.ErrorPresenterFunc + resolverHook graphql.ResolverMiddleware + requestHook graphql.RequestMiddleware } type Option func(cfg *Config) @@ -42,9 +42,12 @@ func RecoverFunc(recover graphql.RecoverFunc) Option { } } -func FormatErrorFunc(f func(error) string) Option { +// ErrorPresenter transforms errors found while resolving into errors that will be returned to the user. It provides +// a good place to add any extra fields, like error.type, that might be desired by your frontend. Check the default +// implementation in graphql.DefaultErrorPresenter for an example. +func ErrorPresenter(f graphql.ErrorPresenterFunc) Option { return func(cfg *Config) { - cfg.formatError = f + cfg.errorPresenter = f } } @@ -88,7 +91,8 @@ func RequestMiddleware(middleware graphql.RequestMiddleware) Option { func GraphQL(exec graphql.ExecutableSchema, options ...Option) http.HandlerFunc { cfg := Config{ - recover: graphql.DefaultRecoverFunc, + recover: graphql.DefaultRecoverFunc, + errorPresenter: graphql.DefaultErrorPresenter, upgrader: websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, @@ -171,8 +175,8 @@ func GraphQL(exec graphql.ExecutableSchema, options ...Option) http.HandlerFunc Recover: cfg.recover, ResolverMiddleware: cfg.resolverHook, RequestMiddleware: cfg.requestHook, - Builder: errors.Builder{ - ErrorMessageFn: cfg.formatError, + ErrorBuilder: graphql.ErrorBuilder{ + ErrorPresenter: cfg.errorPresenter, }, }) @@ -202,8 +206,12 @@ func GraphQL(exec graphql.ExecutableSchema, options ...Option) http.HandlerFunc }) } -func sendError(w http.ResponseWriter, code int, errs ...*errors.QueryError) { +func sendError(w http.ResponseWriter, code int, errors ...*errors.QueryError) { w.WriteHeader(code) + var errs []error + for _, err := range errors { + errs = append(errs, err) + } b, err := json.Marshal(&graphql.Response{Errors: errs}) if err != nil { panic(err) diff --git a/handler/stub.go b/handler/stub.go index 2c7fcf1873e..63785680260 100644 --- a/handler/stub.go +++ b/handler/stub.go @@ -5,7 +5,6 @@ import ( "time" "github.com/vektah/gqlgen/graphql" - "github.com/vektah/gqlgen/neelance/errors" "github.com/vektah/gqlgen/neelance/query" "github.com/vektah/gqlgen/neelance/schema" ) @@ -28,9 +27,7 @@ func (e *executableSchemaStub) Query(ctx context.Context, op *query.Operation) * } func (e *executableSchemaStub) Mutation(ctx context.Context, op *query.Operation) *graphql.Response { - return &graphql.Response{ - Errors: []*errors.QueryError{{Message: "mutations are not supported"}}, - } + return graphql.ErrorResponse(ctx, "mutations are not supported") } func (e *executableSchemaStub) Subscription(ctx context.Context, op *query.Operation) func() *graphql.Response { diff --git a/handler/websocket.go b/handler/websocket.go index 92a9e598289..2b89d256ed2 100644 --- a/handler/websocket.go +++ b/handler/websocket.go @@ -1,7 +1,6 @@ package handler import ( - "bytes" "context" "encoding/json" "fmt" @@ -162,8 +161,8 @@ func (c *wsConnection) subscribe(message *operationMessage) bool { Recover: c.cfg.recover, ResolverMiddleware: c.cfg.resolverHook, RequestMiddleware: c.cfg.requestHook, - Builder: errors.Builder{ - ErrorMessageFn: c.cfg.formatError, + ErrorBuilder: graphql.ErrorBuilder{ + ErrorPresenter: c.cfg.errorPresenter, }, }) @@ -218,19 +217,24 @@ func (c *wsConnection) sendData(id string, response *graphql.Response) { } func (c *wsConnection) sendError(id string, errors ...*errors.QueryError) { - writer := graphql.MarshalErrors(errors) - var b bytes.Buffer - writer.MarshalGQL(&b) - - c.write(&operationMessage{Type: errorMsg, ID: id, Payload: b.Bytes()}) + var errs []error + for _, err := range errors { + errs = append(errs, err) + } + b, err := json.Marshal(errs) + if err != nil { + panic(err) + } + c.write(&operationMessage{Type: errorMsg, ID: id, Payload: b}) } func (c *wsConnection) sendConnectionError(format string, args ...interface{}) { - writer := graphql.MarshalError(&errors.QueryError{Message: fmt.Sprintf(format, args...)}) - var b bytes.Buffer - writer.MarshalGQL(&b) + b, err := json.Marshal(&graphql.ResolverError{Message: fmt.Sprintf(format, args...)}) + if err != nil { + panic(err) + } - c.write(&operationMessage{Type: connectionErrorMsg, Payload: b.Bytes()}) + c.write(&operationMessage{Type: connectionErrorMsg, Payload: b}) } func (c *wsConnection) readOp() *operationMessage { diff --git a/neelance/errors/errors.go b/neelance/errors/errors.go index a442b8b96d3..fdfa62024db 100644 --- a/neelance/errors/errors.go +++ b/neelance/errors/errors.go @@ -27,15 +27,6 @@ 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 { - return &QueryError{ - Message: fmt.Sprintf(format, a...), - ResolverError: err, - } -} - func (err *QueryError) Error() string { if err == nil { return "" @@ -48,26 +39,3 @@ func (err *QueryError) Error() string { } var _ error = &QueryError{} - -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 -} - -func (c *Builder) Errorf(format string, args ...interface{}) { - c.Errors = append(c.Errors, Errorf(format, args...)) -} - -func (c *Builder) Error(err error) { - var gqlErrMessage string - if c.ErrorMessageFn != nil { - gqlErrMessage = c.ErrorMessageFn(err) - } else { - gqlErrMessage = err.Error() - } - c.Errors = append(c.Errors, WithMessagef(err, gqlErrMessage)) -} diff --git a/neelance/errors/errors_test.go b/neelance/errors/errors_test.go deleted file mode 100644 index 698de10ef98..00000000000 --- a/neelance/errors/errors_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package errors - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestBuilder_Error(t *testing.T) { - t.Run("with err converter", func(t *testing.T) { - b := Builder{ErrorMessageFn: convertErr} - b.Error(&testErr{"err1"}) - b.Error(&publicErr{ - message: "err2", - public: "err2 public", - }) - - require.Len(t, b.Errors, 2) - assert.EqualError(t, b.Errors[0], "graphql: err1") - assert.EqualError(t, b.Errors[1], "graphql: err2 public") - }) - t.Run("without err converter", func(t *testing.T) { - var b Builder - b.Error(&testErr{"err1"}) - b.Error(&publicErr{ - message: "err2", - public: "err2 public", - }) - - require.Len(t, b.Errors, 2) - assert.EqualError(t, b.Errors[0], "graphql: err1") - assert.EqualError(t, b.Errors[1], "graphql: err2") - }) -} - -type testErr struct { - message string -} - -func (err *testErr) Error() string { - return err.message -} - -type publicErr struct { - message string - public string -} - -func (err *publicErr) Error() string { - return err.message -} - -func (err *publicErr) PublicError() string { - return err.public -} - -func convertErr(err error) string { - if errConv, ok := err.(*publicErr); ok { - return errConv.public - } - return err.Error() -} diff --git a/test/element.go b/test/element.go new file mode 100644 index 00000000000..860109e6b1b --- /dev/null +++ b/test/element.go @@ -0,0 +1,5 @@ +package test + +type Element struct { + ID int +} diff --git a/test/resolvers_test.go b/test/resolvers_test.go index edf119e2261..5891cdf34bf 100644 --- a/test/resolvers_test.go +++ b/test/resolvers_test.go @@ -5,82 +5,56 @@ package test import ( "context" fmt "fmt" + "net/http/httptest" "testing" + "time" "github.com/pkg/errors" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/vektah/gqlgen/client" "github.com/vektah/gqlgen/graphql" - gqlerrors "github.com/vektah/gqlgen/neelance/errors" - "github.com/vektah/gqlgen/neelance/query" + "github.com/vektah/gqlgen/handler" "github.com/vektah/gqlgen/test/introspection" invalid_identifier "github.com/vektah/gqlgen/test/invalid-identifier" "github.com/vektah/gqlgen/test/models" ) -func TestCompiles(t *testing.T) {} - -func TestErrorConverter(t *testing.T) { +func TestCustomErrorPresenter(t *testing.T) { resolvers := &testResolvers{} - s := MakeExecutableSchema(resolvers) - - doc, errs := query.Parse(`query { nestedOutputs { inner { id } } } `) - require.Nil(t, errs) - - t.Run("with", func(t *testing.T) { - testConvErr := func(e error) string { + srv := httptest.NewServer(handler.GraphQL(MakeExecutableSchema(resolvers), + handler.ErrorPresenter(func(i context.Context, e error) error { if _, ok := errors.Cause(e).(*specialErr); ok { - return "override special error message" + return &graphql.ResolverError{Message: "override special error message"} } - return e.Error() - } - t.Run("special error", func(t *testing.T) { - resolvers.nestedOutputsErr = &specialErr{} - - resp := s.Query(mkctx(doc, testConvErr), doc.Operations[0]) - require.Len(t, resp.Errors, 1) - assert.Equal(t, "override special error message", resp.Errors[0].Message) - }) - t.Run("normal error", func(t *testing.T) { - resolvers.nestedOutputsErr = fmt.Errorf("a normal error") - - resp := s.Query(mkctx(doc, testConvErr), doc.Operations[0]) - require.Len(t, resp.Errors, 1) - assert.Equal(t, "a normal error", resp.Errors[0].Message) - }) + return &graphql.ResolverError{Message: e.Error()} + }), + )) + c := client.New(srv.URL) + + t.Run("special error", func(t *testing.T) { + resolvers.nestedOutputsErr = &specialErr{} + var resp struct{} + err := c.Post(`query { nestedOutputs { inner { id } } }`, &resp) + + assert.EqualError(t, err, `[{"message":"override special error message"}]`) }) + t.Run("normal error", func(t *testing.T) { + resolvers.nestedOutputsErr = fmt.Errorf("a normal error") + var resp struct{} + err := c.Post(`query { nestedOutputs { inner { id } } }`, &resp) - t.Run("without", func(t *testing.T) { - t.Run("special error", func(t *testing.T) { - resolvers.nestedOutputsErr = &specialErr{} - - resp := s.Query(mkctx(doc, nil), doc.Operations[0]) - require.Len(t, resp.Errors, 1) - assert.Equal(t, "original special error message", resp.Errors[0].Message) - }) - t.Run("normal error", func(t *testing.T) { - resolvers.nestedOutputsErr = fmt.Errorf("a normal error") - - resp := s.Query(mkctx(doc, nil), doc.Operations[0]) - require.Len(t, resp.Errors, 1) - assert.Equal(t, "a normal error", resp.Errors[0].Message) - }) + assert.EqualError(t, err, `[{"message":"a normal error"}]`) }) } -func mkctx(doc *query.Document, errFn func(e error) string) context.Context { - return graphql.WithRequestContext(context.Background(), &graphql.RequestContext{ - Doc: doc, - ResolverMiddleware: func(ctx context.Context, next graphql.Resolver) (res interface{}, err error) { - return next(ctx) - }, - RequestMiddleware: func(ctx context.Context, next func(ctx context.Context) []byte) []byte { - return next(ctx) - }, - Builder: gqlerrors.Builder{ - ErrorMessageFn: errFn, - }, - }) +func TestErrorPath(t *testing.T) { + srv := httptest.NewServer(handler.GraphQL(MakeExecutableSchema(&testResolvers{}))) + c := client.New(srv.URL) + + var resp struct{} + err := c.Post(`{ path { cc:child { error(message: "boom") } } }`, &resp) + + assert.EqualError(t, err, `[{"message":"boom","path":["path",0,"cc","error"]},{"message":"boom","path":["path",1,"cc","error"]},{"message":"boom","path":["path",2,"cc","error"]},{"message":"boom","path":["path",3,"cc","error"]}]`) } type testResolvers struct { @@ -125,6 +99,24 @@ func (r *testResolvers) Query_invalidIdentifier(ctx context.Context) (*invalid_i return r.invalidIdentifier, nil } +func (r *testResolvers) Query_path(ctx context.Context) ([]Element, error) { + return []Element{{1}, {2}, {3}, {4}}, nil +} + +func (r *testResolvers) Element_child(ctx context.Context, obj *Element) (Element, error) { + return Element{obj.ID * 10}, nil +} + +func (r *testResolvers) Element_error(ctx context.Context, obj *Element, message *string) (bool, error) { + // A silly hack to make the result order stable + time.Sleep(time.Duration(obj.ID) * 10 * time.Millisecond) + + if message != nil { + return true, errors.New(*message) + } + return false, nil +} + type specialErr struct{} func (*specialErr) Error() string { diff --git a/test/schema.graphql b/test/schema.graphql index 52d9c475a4b..3bd64cb659e 100644 --- a/test/schema.graphql +++ b/test/schema.graphql @@ -48,6 +48,11 @@ type InvalidIdentifier { id: Int! } +type Element { + child: Element! + error(message: String): Boolean! +} + type Query { nestedInputs(input: [[OuterInput]] = [[{inner: {id: 1}}]]): Boolean nestedOutputs: [[OuterObject]] @@ -56,4 +61,5 @@ type Query { mapInput(input: Changes): Boolean collision: It invalidIdentifier: InvalidIdentifier + path: [Element] } diff --git a/test/types.json b/test/types.json index 58035934ffa..6d14290bb6c 100644 --- a/test/types.json +++ b/test/types.json @@ -6,5 +6,6 @@ "RecursiveInputSlice": "github.com/vektah/gqlgen/test.RecursiveInputSlice", "It": "github.com/vektah/gqlgen/test/introspection.It", "Changes": "map[string]interface{}", - "InvalidIdentifier": "github.com/vektah/gqlgen/test/invalid-identifier.InvalidIdentifier" + "InvalidIdentifier": "github.com/vektah/gqlgen/test/invalid-identifier.InvalidIdentifier", + "Element": "github.com/vektah/gqlgen/test.Element" }