Skip to content

Commit 7e9c542

Browse files
akupilasetnicka
authored andcommitted
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 fabd9b3 commit 7e9c542

File tree

3 files changed

+145
-11
lines changed

3 files changed

+145
-11
lines changed

graphql.go

Lines changed: 51 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,17 @@ 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+
// if gqlErrors, ok := err.(graphql.Errors); ok {
88+
// for _, e := range gqlErrors {
89+
// // Server returned an error
90+
// }
91+
// }
92+
// // Another error occurred
93+
// }
8494
func (c *Client) Run(ctx context.Context, req *Request, resp interface{}) error {
8595
select {
8696
case <-ctx.Done():
@@ -146,8 +156,7 @@ func (c *Client) runWithJSON(ctx context.Context, req *Request, resp interface{}
146156
return errors.Wrap(err, "decoding response")
147157
}
148158
if len(gr.Errors) > 0 {
149-
// return first error
150-
return gr.Errors[0]
159+
return gr.Errors
151160
}
152161

153162
// Handle the http status codes before handling response from the graphql endpoint.
@@ -230,8 +239,7 @@ func (c *Client) runWithPostFields(ctx context.Context, req *Request, resp inter
230239
return errors.Wrap(err, "decoding response")
231240
}
232241
if len(gr.Errors) > 0 {
233-
// return first error
234-
return gr.Errors[0]
242+
return gr.Errors
235243
}
236244

237245
// Handle the http status codes before handling response from the graphql endpoint.
@@ -274,17 +282,51 @@ func ImmediatelyCloseReqBody() ClientOption {
274282
// modify the behaviour of the Client.
275283
type ClientOption func(*Client)
276284

277-
type graphErr struct {
285+
// Errors contains all the errors that were returned by the GraphQL server.
286+
type Errors []Error
287+
288+
func (ee Errors) Error() string {
289+
if len(ee) == 0 {
290+
return "no errors"
291+
}
292+
errs := make([]string, len(ee))
293+
for i, e := range ee {
294+
errs[i] = e.Message
295+
}
296+
return "graphql: " + strings.Join(errs, "; ")
297+
}
298+
299+
// An Error contains error information returned by the GraphQL server.
300+
type Error struct {
301+
// Message contains the error message.
278302
Message string
303+
// Locations contains the locations in the GraphQL document that caused the
304+
// error if the error can be associated to a particular point in the
305+
// requested GraphQL document.
306+
Locations []Location
307+
// Path contains the key path of the response field which experienced the
308+
// error. This allows clients to identify whether a nil result is
309+
// intentional or caused by a runtime error.
310+
Path []interface{}
311+
// Extensions may contain additional fields set by the GraphQL service,
312+
// such as an error code.
313+
Extensions map[string]interface{}
314+
}
315+
316+
// A Location is a location in the GraphQL query that resulted in an error.
317+
// The location may be returned as part of an error response.
318+
type Location struct {
319+
Line int
320+
Column int
279321
}
280322

281-
func (e graphErr) Error() string {
323+
func (e Error) Error() string {
282324
return "graphql: " + e.Message
283325
}
284326

285327
type graphResponse struct {
286328
Data interface{}
287-
Errors []graphErr
329+
Errors Errors
288330
}
289331

290332
// 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)