Skip to content

Commit 6010b63

Browse files
HTTPError wraps full Response for typed output (#366)
Follow-up to #363. Go one step further lets HTTPError wrap Response to get fully typed error data. I have: - [x] Written a clear PR title and description (above) - [x] Signed the [Khan Academy CLA](https://www.khanacademy.org/r/cla) - [x] Added tests covering my changes, if applicable - [x] Included a link to the issue fixed, if applicable - [x] Included documentation, for new features - [x] Added an entry to the changelog
1 parent d3e516b commit 6010b63

File tree

3 files changed

+65
-26
lines changed

3 files changed

+65
-26
lines changed

graphql/client.go

+12-1
Original file line numberDiff line numberDiff line change
@@ -265,9 +265,20 @@ func (c *client) MakeRequest(ctx context.Context, req *Request, resp *Response)
265265
if err != nil {
266266
respBody = []byte(fmt.Sprintf("<unreadable: %v>", err))
267267
}
268+
269+
var gqlResp Response
270+
if err = json.Unmarshal(respBody, &gqlResp); err != nil {
271+
return &HTTPError{
272+
Response: Response{
273+
Errors: gqlerror.List{&gqlerror.Error{Message: string(respBody)}},
274+
},
275+
StatusCode: httpResp.StatusCode,
276+
}
277+
}
278+
268279
return &HTTPError{
280+
Response: gqlResp,
269281
StatusCode: httpResp.StatusCode,
270-
Body: string(respBody),
271282
}
272283
}
273284

graphql/client_test.go

+41-21
Original file line numberDiff line numberDiff line change
@@ -9,44 +9,65 @@ import (
99
"testing"
1010

1111
"github.com/stretchr/testify/assert"
12+
"github.com/vektah/gqlparser/v2/gqlerror"
1213
)
1314

1415
func TestMakeRequest_HTTPError(t *testing.T) {
1516
testCases := []struct {
17+
expectedError *HTTPError
18+
serverResponseBody any
1619
name string
17-
serverResponseBody string
18-
expectedErrorBody string
1920
serverResponseCode int
20-
expectedStatusCode int
2121
}{
2222
{
23-
name: "400 Bad Request",
24-
serverResponseBody: "Bad Request",
25-
expectedErrorBody: "Bad Request",
23+
name: "plain_text_error",
2624
serverResponseCode: http.StatusBadRequest,
27-
expectedStatusCode: http.StatusBadRequest,
25+
serverResponseBody: "Bad Request",
26+
expectedError: &HTTPError{
27+
Response: Response{
28+
Errors: gqlerror.List{
29+
&gqlerror.Error{
30+
Message: "\"Bad Request\"\n",
31+
},
32+
},
33+
},
34+
StatusCode: http.StatusBadRequest,
35+
},
2836
},
2937
{
30-
name: "429 Too Many Requests",
31-
serverResponseBody: "Rate limit exceeded",
32-
expectedErrorBody: "Rate limit exceeded",
38+
name: "json_error_with_extensions",
3339
serverResponseCode: http.StatusTooManyRequests,
34-
expectedStatusCode: http.StatusTooManyRequests,
35-
},
36-
{
37-
name: "500 Internal Server Error",
38-
serverResponseBody: "Internal Server Error",
39-
expectedErrorBody: "Internal Server Error",
40-
serverResponseCode: http.StatusInternalServerError,
41-
expectedStatusCode: http.StatusInternalServerError,
40+
serverResponseBody: Response{
41+
Errors: gqlerror.List{
42+
&gqlerror.Error{
43+
Message: "Rate limit exceeded",
44+
Extensions: map[string]interface{}{
45+
"code": "RATE_LIMIT_EXCEEDED",
46+
},
47+
},
48+
},
49+
},
50+
expectedError: &HTTPError{
51+
Response: Response{
52+
Errors: gqlerror.List{
53+
&gqlerror.Error{
54+
Message: "Rate limit exceeded",
55+
Extensions: map[string]interface{}{
56+
"code": "RATE_LIMIT_EXCEEDED",
57+
},
58+
},
59+
},
60+
},
61+
StatusCode: http.StatusTooManyRequests,
62+
},
4263
},
4364
}
4465

4566
for _, tc := range testCases {
4667
t.Run(tc.name, func(t *testing.T) {
4768
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
4869
w.WriteHeader(tc.serverResponseCode)
49-
_, err := w.Write([]byte(tc.serverResponseBody))
70+
err := json.NewEncoder(w).Encode(tc.serverResponseBody)
5071
if err != nil {
5172
t.Fatalf("Failed to write response: %v", err)
5273
}
@@ -64,8 +85,7 @@ func TestMakeRequest_HTTPError(t *testing.T) {
6485
assert.Error(t, err)
6586
var httpErr *HTTPError
6687
assert.True(t, errors.As(err, &httpErr), "Error should be of type *HTTPError")
67-
assert.Equal(t, tc.expectedStatusCode, httpErr.StatusCode)
68-
assert.Equal(t, tc.expectedErrorBody, httpErr.Body)
88+
assert.Equal(t, tc.expectedError, httpErr)
6989
})
7090
}
7191
}

graphql/errors.go

+12-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
package graphql
22

3-
import "fmt"
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
)
47

5-
// HTTPError represents an HTTP error with status code and response body.
8+
// HTTPError represents an HTTP error with status coqgqde and response body.
69
type HTTPError struct {
7-
Body string
10+
Response Response
811
StatusCode int
912
}
1013

1114
// Error implements the error interface for HTTPError.
1215
func (e *HTTPError) Error() string {
13-
return fmt.Sprintf("returned error %v: '%s'", e.StatusCode, e.Body)
16+
jsonBody, err := json.Marshal(e.Response)
17+
if err != nil {
18+
return fmt.Sprintf("returned error %v: '%s'", e.StatusCode, e.Response)
19+
}
20+
21+
return fmt.Sprintf("returned error %v: %s", e.StatusCode, jsonBody)
1422
}

0 commit comments

Comments
 (0)