Skip to content

Commit

Permalink
Add fully customizable resolver errors
Browse files Browse the repository at this point in the history
  • Loading branch information
vektah committed Apr 27, 2018
1 parent a0f66c8 commit 5b6331f
Show file tree
Hide file tree
Showing 25 changed files with 275 additions and 246 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
26 changes: 15 additions & 11 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"net/http"

"github.com/mitchellh/mapstructure"
"github.com/vektah/gqlgen/neelance/errors"
)

// Client for graphql requests
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion codegen/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
9 changes: 7 additions & 2 deletions codegen/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions codegen/templates/args.gotpl
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand All @@ -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 }}
Expand Down
30 changes: 19 additions & 11 deletions codegen/templates/field.gotpl
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 }}
Expand All @@ -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 {
Expand Down
6 changes: 3 additions & 3 deletions codegen/templates/generated.gotpl
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
}

Expand All @@ -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 }}
}

Expand Down Expand Up @@ -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 }}
}

Expand Down
11 changes: 9 additions & 2 deletions codegen/templates/object.gotpl
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion example/scalars/scalar_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion example/starwars/starwars_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 2 additions & 1 deletion example/todo/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}),
Expand Down
2 changes: 1 addition & 1 deletion example/todo/todo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
25 changes: 23 additions & 2 deletions graphql/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package graphql
import (
"context"

"github.com/vektah/gqlgen/neelance/errors"
"github.com/vektah/gqlgen/neelance/query"
)

Expand All @@ -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{}
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}

Expand Down
51 changes: 28 additions & 23 deletions graphql/error.go
Original file line number Diff line number Diff line change
@@ -1,36 +1,41 @@
package graphql

import (
"github.com/vektah/gqlgen/neelance/errors"
"context"
"fmt"
)

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
}

locations = append(locations, locationObj)
}
func (c *ErrorBuilder) Errorf(ctx context.Context, format string, args ...interface{}) {
c.Errors = append(c.Errors, c.ErrorPresenter(ctx, fmt.Errorf(format, args...)))
}

errObj.Add("locations", locations)
}
return errObj
func (c *ErrorBuilder) Error(ctx context.Context, err error) {
c.Errors = append(c.Errors, c.ErrorPresenter(ctx, err))
}
Loading

0 comments on commit 5b6331f

Please sign in to comment.