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

expose details for returned errors #2

Merged
merged 1 commit into from
Feb 2, 2023
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
60 changes: 51 additions & 9 deletions graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
"io"
"mime/multipart"
"net/http"
"strings"

"github.com/pkg/errors"
)
Expand Down Expand Up @@ -79,8 +80,17 @@ func (c *Client) logf(format string, args ...interface{}) {
// Run executes the query and unmarshals the response from the data field
// into the response object.
// Pass in a nil response object to skip response parsing.
// If the request fails or the server returns an error, the first error
// will be returned.
// If the request fails or the server returns an error, the returned error will
// be of type Errors. Type assert to get the underlying errors:
// err := client.Run(..)
// if err != nil {
// if gqlErrors, ok := err.(graphql.Errors); ok {
// for _, e := range gqlErrors {
// // Server returned an error
// }
// }
// // Another error occurred
// }
func (c *Client) Run(ctx context.Context, req *Request, resp interface{}) error {
select {
case <-ctx.Done():
Expand Down Expand Up @@ -144,8 +154,7 @@ func (c *Client) runWithJSON(ctx context.Context, req *Request, resp interface{}
return errors.Wrap(err, "decoding response")
}
if len(gr.Errors) > 0 {
// return first error
return gr.Errors[0]
return gr.Errors
}
return nil
}
Expand Down Expand Up @@ -215,8 +224,7 @@ func (c *Client) runWithPostFields(ctx context.Context, req *Request, resp inter
return errors.Wrap(err, "decoding response")
}
if len(gr.Errors) > 0 {
// return first error
return gr.Errors[0]
return gr.Errors
}
return nil
}
Expand Down Expand Up @@ -249,17 +257,51 @@ func ImmediatelyCloseReqBody() ClientOption {
// modify the behaviour of the Client.
type ClientOption func(*Client)

type graphErr struct {
// Errors contains all the errors that were returned by the GraphQL server.
type Errors []Error

func (ee Errors) Error() string {
if len(ee) == 0 {
return "no errors"
}
errs := make([]string, len(ee))
for i, e := range ee {
errs[i] = e.Message
}
return "graphql: " + strings.Join(errs, "; ")
}

// An Error contains error information returned by the GraphQL server.
type Error struct {
// Message contains the error message.
Message string
// Locations contains the locations in the GraphQL document that caused the
// error if the error can be associated to a particular point in the
// requested GraphQL document.
Locations []Location
// Path contains the key path of the response field which experienced the
// error. This allows clients to identify whether a nil result is
// intentional or caused by a runtime error.
Path []interface{}
// Extensions may contain additional fields set by the GraphQL service,
// such as an error code.
Extensions map[string]interface{}
}

// A Location is a location in the GraphQL query that resulted in an error.
// The location may be returned as part of an error response.
type Location struct {
Line int
Column int
}

func (e graphErr) Error() string {
func (e Error) Error() string {
return "graphql: " + e.Message
}

type graphResponse struct {
Data interface{}
Errors []graphErr
Errors Errors
}

// Request is a GraphQL request.
Expand Down
49 changes: 48 additions & 1 deletion graphql_json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ func TestDoJSONBadRequestErr(t *testing.T) {
io.WriteString(w, `{
"errors": [{
"message": "miscellaneous message as to why the the request was bad"
}, {
"message": "another error"
}]
}`)
}))
Expand All @@ -92,7 +94,52 @@ func TestDoJSONBadRequestErr(t *testing.T) {
var responseData map[string]interface{}
err := client.Run(ctx, &Request{q: "query {}"}, &responseData)
is.Equal(calls, 1) // calls
is.Equal(err.Error(), "graphql: miscellaneous message as to why the the request was bad")
is.Equal(err.Error(), "graphql: miscellaneous message as to why the the request was bad; another error")
}

func TestDoJSONBadRequestErrDetails(t *testing.T) {
is := is.New(t)
var calls int
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
is.Equal(r.Method, http.MethodPost)
b, err := ioutil.ReadAll(r.Body)
is.NoErr(err)
is.Equal(string(b), `{"query":"query {}","variables":null}`+"\n")
w.WriteHeader(http.StatusBadRequest)
io.WriteString(w, `{
"errors": [{
"message": "Name for character with ID 1002 could not be fetched.",
"locations": [ { "line": 6, "column": 7 } ],
"path": [ "hero", "heroFriends", 1, "name" ],
"extensions": {
"code": "CAN_NOT_FETCH_BY_ID",
"timestamp": "Fri Feb 9 14:33:09 UTC 2018"
}
}]
}`)
}))
defer srv.Close()

ctx := context.Background()
client := NewClient(srv.URL)

ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
defer cancel()
var responseData map[string]interface{}
err := client.Run(ctx, &Request{q: "query {}"}, &responseData)
is.Equal(calls, 1) // calls
errs, ok := err.(Errors)
is.True(ok)
is.Equal(len(errs), 1)
e := errs[0]
is.Equal(e.Message, "Name for character with ID 1002 could not be fetched.")
is.Equal(e.Locations, []Location{{Line: 6, Column: 7}})
is.Equal(e.Path, []interface{}{"hero", "heroFriends", 1.0, "name"})
is.Equal(e.Extensions, map[string]interface{}{
"code": "CAN_NOT_FETCH_BY_ID",
"timestamp": "Fri Feb 9 14:33:09 UTC 2018",
})
}

func TestQueryJSON(t *testing.T) {
Expand Down
47 changes: 46 additions & 1 deletion graphql_multipart_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ func TestDoErr(t *testing.T) {
io.WriteString(w, `{
"errors": [{
"message": "Something went wrong"
}, {
"message": "Something else went wrong"
}]
}`)
}))
Expand All @@ -114,7 +116,7 @@ func TestDoErr(t *testing.T) {
var responseData map[string]interface{}
err := client.Run(ctx, &Request{q: "query {}"}, &responseData)
is.True(err != nil)
is.Equal(err.Error(), "graphql: Something went wrong")
is.Equal(err.Error(), "graphql: Something went wrong; Something else went wrong")
}

func TestDoServerErr(t *testing.T) {
Expand Down Expand Up @@ -167,6 +169,49 @@ func TestDoBadRequestErr(t *testing.T) {
is.Equal(err.Error(), "graphql: miscellaneous message as to why the the request was bad")
}

func TestDoBadRequestErrDetails(t *testing.T) {
is := is.New(t)
var calls int
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
is.Equal(r.Method, http.MethodPost)
query := r.FormValue("query")
is.Equal(query, `query {}`)
w.WriteHeader(http.StatusBadRequest)
io.WriteString(w, `{
"errors": [{
"message": "Name for character with ID 1002 could not be fetched.",
"locations": [ { "line": 6, "column": 7 } ],
"path": [ "hero", "heroFriends", 1, "name" ],
"extensions": {
"code": "CAN_NOT_FETCH_BY_ID",
"timestamp": "Fri Feb 9 14:33:09 UTC 2018"
}
}]
}`)
}))
defer srv.Close()

ctx := context.Background()
client := NewClient(srv.URL, UseMultipartForm())

ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
defer cancel()
var responseData map[string]interface{}
err := client.Run(ctx, &Request{q: "query {}"}, &responseData)
errs, ok := err.(Errors)
is.True(ok)
is.Equal(len(errs), 1)
e := errs[0]
is.Equal(e.Message, "Name for character with ID 1002 could not be fetched.")
is.Equal(e.Locations, []Location{{Line: 6, Column: 7}})
is.Equal(e.Path, []interface{}{"hero", "heroFriends", 1.0, "name"})
is.Equal(e.Extensions, map[string]interface{}{
"code": "CAN_NOT_FETCH_BY_ID",
"timestamp": "Fri Feb 9 14:33:09 UTC 2018",
})
}

func TestDoNoResponse(t *testing.T) {
is := is.New(t)
var calls int
Expand Down