Skip to content

Commit 77fa3ac

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 77fa3ac

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():
@@ -144,8 +154,7 @@ func (c *Client) runWithJSON(ctx context.Context, req *Request, resp interface{}
144154
return errors.Wrap(err, "decoding response")
145155
}
146156
if len(gr.Errors) > 0 {
147-
// return first error
148-
return gr.Errors[0]
157+
return gr.Errors
149158
}
150159
return nil
151160
}
@@ -215,8 +224,7 @@ func (c *Client) runWithPostFields(ctx context.Context, req *Request, resp inter
215224
return errors.Wrap(err, "decoding response")
216225
}
217226
if len(gr.Errors) > 0 {
218-
// return first error
219-
return gr.Errors[0]
227+
return gr.Errors
220228
}
221229
return nil
222230
}
@@ -249,17 +257,51 @@ func ImmediatelyCloseReqBody() ClientOption {
249257
// modify the behaviour of the Client.
250258
type ClientOption func(*Client)
251259

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

256-
func (e graphErr) Error() string {
298+
func (e Error) Error() string {
257299
return "graphql: " + e.Message
258300
}
259301

260302
type graphResponse struct {
261303
Data interface{}
262-
Errors []graphErr
304+
Errors Errors
263305
}
264306

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