Skip to content

Commit 8926c51

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 8926c51

File tree

3 files changed

+134
-9
lines changed

3 files changed

+134
-9
lines changed

graphql.go

Lines changed: 40 additions & 7 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
)
@@ -144,8 +145,7 @@ func (c *Client) runWithJSON(ctx context.Context, req *Request, resp interface{}
144145
return errors.Wrap(err, "decoding response")
145146
}
146147
if len(gr.Errors) > 0 {
147-
// return first error
148-
return gr.Errors[0]
148+
return gr.Errors
149149
}
150150
return nil
151151
}
@@ -215,8 +215,7 @@ func (c *Client) runWithPostFields(ctx context.Context, req *Request, resp inter
215215
return errors.Wrap(err, "decoding response")
216216
}
217217
if len(gr.Errors) > 0 {
218-
// return first error
219-
return gr.Errors[0]
218+
return gr.Errors
220219
}
221220
return nil
222221
}
@@ -249,17 +248,51 @@ func ImmediatelyCloseReqBody() ClientOption {
249248
// modify the behaviour of the Client.
250249
type ClientOption func(*Client)
251250

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

256-
func (e graphErr) Error() string {
289+
func (e Error) Error() string {
257290
return "graphql: " + e.Message
258291
}
259292

260293
type graphResponse struct {
261294
Data interface{}
262-
Errors []graphErr
295+
Errors Errors
263296
}
264297

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