Skip to content

Commit 6ac6ff8

Browse files
committed
expose details for returned errors
Exposes the error as graphql.Error and returns error details (path, location, extensions) in case they are present in the error response. The format matches the June 2018 spec for errors: https://graphql.github.io/graphql-spec/June2018/#sec-Errors
1 parent 3a92531 commit 6ac6ff8

File tree

3 files changed

+142
-11
lines changed

3 files changed

+142
-11
lines changed

graphql.go

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838
"io"
3939
"mime/multipart"
4040
"net/http"
41+
"strings"
4142

4243
"github.com/pkg/errors"
4344
)
@@ -79,8 +80,14 @@ func (c *Client) logf(format string, args ...interface{}) {
7980
// Run executes the query and unmarshals the response from the data field
8081
// into the response object.
8182
// Pass in a nil response object to skip response parsing.
82-
// If the request fails or the server returns an error, the first error
83-
// will be returned.
83+
// If the request fails or the server returns an error, the returned error will
84+
// be of type Errors. Type assert to get the underlying errors:
85+
// err := client.Run(..)
86+
// if err != nil {
87+
// for _, e := range err.(graphql.Errors) {
88+
// // ..
89+
// }
90+
// }
8491
func (c *Client) Run(ctx context.Context, req *Request, resp interface{}) error {
8592
select {
8693
case <-ctx.Done():
@@ -144,8 +151,7 @@ func (c *Client) runWithJSON(ctx context.Context, req *Request, resp interface{}
144151
return errors.Wrap(err, "decoding response")
145152
}
146153
if len(gr.Errors) > 0 {
147-
// return first error
148-
return gr.Errors[0]
154+
return gr.Errors
149155
}
150156
return nil
151157
}
@@ -215,8 +221,7 @@ func (c *Client) runWithPostFields(ctx context.Context, req *Request, resp inter
215221
return errors.Wrap(err, "decoding response")
216222
}
217223
if len(gr.Errors) > 0 {
218-
// return first error
219-
return gr.Errors[0]
224+
return gr.Errors
220225
}
221226
return nil
222227
}
@@ -249,17 +254,51 @@ func ImmediatelyCloseReqBody() ClientOption {
249254
// modify the behaviour of the Client.
250255
type ClientOption func(*Client)
251256

252-
type graphErr struct {
257+
// Errors contains all the errors that were returned by the GraphQL server.
258+
type Errors []Error
259+
260+
func (ee Errors) Error() string {
261+
if len(ee) == 0 {
262+
return "no errors"
263+
}
264+
errs := make([]string, len(ee))
265+
for i, e := range ee {
266+
errs[i] = e.Message
267+
}
268+
return "graphql: " + strings.Join(errs, "; ")
269+
}
270+
271+
// An Error contains error information returned by the GraphQL server.
272+
type Error struct {
273+
// Message contains the error message.
253274
Message string
275+
// Locations contains the locations in the GraphQL document that caused the
276+
// error if the error can be associated to a particular point in the
277+
// requested GraphQL document.
278+
Locations []Location
279+
// Path contains the key path of the response field which experienced the
280+
// error. This allows clients to identify whether a nil result is
281+
// intentional or caused by a runtime error.
282+
Path []interface{}
283+
// Extensions may contain additional fields set by the GraphQL service,
284+
// such as an error code.
285+
Extensions map[string]interface{}
286+
}
287+
288+
// A Location is a location in the GraphQL query that resulted in an error.
289+
// The location may be returned as part of an error response.
290+
type Location struct {
291+
Line int
292+
Column int
254293
}
255294

256-
func (e graphErr) Error() string {
295+
func (e Error) Error() string {
257296
return "graphql: " + e.Message
258297
}
259298

260299
type graphResponse struct {
261300
Data interface{}
262-
Errors []graphErr
301+
Errors Errors
263302
}
264303

265304
// Request is a GraphQL request.

graphql_json_test.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ func TestDoJSONBadRequestErr(t *testing.T) {
7979
io.WriteString(w, `{
8080
"errors": [{
8181
"message": "miscellaneous message as to why the the request was bad"
82+
}, {
83+
"message": "another error"
8284
}]
8385
}`)
8486
}))
@@ -92,7 +94,52 @@ func TestDoJSONBadRequestErr(t *testing.T) {
9294
var responseData map[string]interface{}
9395
err := client.Run(ctx, &Request{q: "query {}"}, &responseData)
9496
is.Equal(calls, 1) // calls
95-
is.Equal(err.Error(), "graphql: miscellaneous message as to why the the request was bad")
97+
is.Equal(err.Error(), "graphql: miscellaneous message as to why the the request was bad; another error")
98+
}
99+
100+
func TestDoJSONBadRequestErrDetails(t *testing.T) {
101+
is := is.New(t)
102+
var calls int
103+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
104+
calls++
105+
is.Equal(r.Method, http.MethodPost)
106+
b, err := ioutil.ReadAll(r.Body)
107+
is.NoErr(err)
108+
is.Equal(string(b), `{"query":"query {}","variables":null}`+"\n")
109+
w.WriteHeader(http.StatusBadRequest)
110+
io.WriteString(w, `{
111+
"errors": [{
112+
"message": "Name for character with ID 1002 could not be fetched.",
113+
"locations": [ { "line": 6, "column": 7 } ],
114+
"path": [ "hero", "heroFriends", 1, "name" ],
115+
"extensions": {
116+
"code": "CAN_NOT_FETCH_BY_ID",
117+
"timestamp": "Fri Feb 9 14:33:09 UTC 2018"
118+
}
119+
}]
120+
}`)
121+
}))
122+
defer srv.Close()
123+
124+
ctx := context.Background()
125+
client := NewClient(srv.URL)
126+
127+
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
128+
defer cancel()
129+
var responseData map[string]interface{}
130+
err := client.Run(ctx, &Request{q: "query {}"}, &responseData)
131+
is.Equal(calls, 1) // calls
132+
errs, ok := err.(Errors)
133+
is.True(ok)
134+
is.Equal(len(errs), 1)
135+
e := errs[0]
136+
is.Equal(e.Message, "Name for character with ID 1002 could not be fetched.")
137+
is.Equal(e.Locations, []Location{{Line: 6, Column: 7}})
138+
is.Equal(e.Path, []interface{}{"hero", "heroFriends", 1.0, "name"})
139+
is.Equal(e.Extensions, map[string]interface{}{
140+
"code": "CAN_NOT_FETCH_BY_ID",
141+
"timestamp": "Fri Feb 9 14:33:09 UTC 2018",
142+
})
96143
}
97144

98145
func TestQueryJSON(t *testing.T) {

graphql_multipart_test.go

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ func TestDoErr(t *testing.T) {
101101
io.WriteString(w, `{
102102
"errors": [{
103103
"message": "Something went wrong"
104+
}, {
105+
"message": "Something else went wrong"
104106
}]
105107
}`)
106108
}))
@@ -114,7 +116,7 @@ func TestDoErr(t *testing.T) {
114116
var responseData map[string]interface{}
115117
err := client.Run(ctx, &Request{q: "query {}"}, &responseData)
116118
is.True(err != nil)
117-
is.Equal(err.Error(), "graphql: Something went wrong")
119+
is.Equal(err.Error(), "graphql: Something went wrong; Something else went wrong")
118120
}
119121

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

172+
func TestDoBadRequestErrDetails(t *testing.T) {
173+
is := is.New(t)
174+
var calls int
175+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
176+
calls++
177+
is.Equal(r.Method, http.MethodPost)
178+
query := r.FormValue("query")
179+
is.Equal(query, `query {}`)
180+
w.WriteHeader(http.StatusBadRequest)
181+
io.WriteString(w, `{
182+
"errors": [{
183+
"message": "Name for character with ID 1002 could not be fetched.",
184+
"locations": [ { "line": 6, "column": 7 } ],
185+
"path": [ "hero", "heroFriends", 1, "name" ],
186+
"extensions": {
187+
"code": "CAN_NOT_FETCH_BY_ID",
188+
"timestamp": "Fri Feb 9 14:33:09 UTC 2018"
189+
}
190+
}]
191+
}`)
192+
}))
193+
defer srv.Close()
194+
195+
ctx := context.Background()
196+
client := NewClient(srv.URL, UseMultipartForm())
197+
198+
ctx, cancel := context.WithTimeout(ctx, 1*time.Second)
199+
defer cancel()
200+
var responseData map[string]interface{}
201+
err := client.Run(ctx, &Request{q: "query {}"}, &responseData)
202+
errs, ok := err.(Errors)
203+
is.True(ok)
204+
is.Equal(len(errs), 1)
205+
e := errs[0]
206+
is.Equal(e.Message, "Name for character with ID 1002 could not be fetched.")
207+
is.Equal(e.Locations, []Location{{Line: 6, Column: 7}})
208+
is.Equal(e.Path, []interface{}{"hero", "heroFriends", 1.0, "name"})
209+
is.Equal(e.Extensions, map[string]interface{}{
210+
"code": "CAN_NOT_FETCH_BY_ID",
211+
"timestamp": "Fri Feb 9 14:33:09 UTC 2018",
212+
})
213+
}
214+
170215
func TestDoNoResponse(t *testing.T) {
171216
is := is.New(t)
172217
var calls int

0 commit comments

Comments
 (0)