Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standardize error return from HTTP requests in genqlient #352

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion graphql/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,10 @@ func (c *client) MakeRequest(ctx context.Context, req *Request, resp *Response)
if err != nil {
respBody = []byte(fmt.Sprintf("<unreadable: %v>", err))
}
return fmt.Errorf("returned error %v: %s", httpResp.Status, respBody)
return &HTTPError{
StatusCode: httpResp.StatusCode,
Body: string(respBody),
}
}

err = json.NewDecoder(httpResp.Body).Decode(resp)
Expand Down
97 changes: 97 additions & 0 deletions graphql/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package graphql

import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
)

func TestMakeRequest_HTTPError(t *testing.T) {
testCases := []struct {
name string
serverResponseBody string
expectedErrorBody string
serverResponseCode int
expectedStatusCode int
}{
{
name: "400 Bad Request",
serverResponseBody: "Bad Request",
expectedErrorBody: "Bad Request",
serverResponseCode: http.StatusBadRequest,
expectedStatusCode: http.StatusBadRequest,
},
{
name: "429 Too Many Requests",
serverResponseBody: "Rate limit exceeded",
expectedErrorBody: "Rate limit exceeded",
serverResponseCode: http.StatusTooManyRequests,
expectedStatusCode: http.StatusTooManyRequests,
},
{
name: "500 Internal Server Error",
serverResponseBody: "Internal Server Error",
expectedErrorBody: "Internal Server Error",
serverResponseCode: http.StatusInternalServerError,
expectedStatusCode: http.StatusInternalServerError,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(tc.serverResponseCode)
_, err := w.Write([]byte(tc.serverResponseBody))
if err != nil {
t.Fatalf("Failed to write response: %v", err)
}
}))
defer server.Close()

client := NewClient(server.URL, server.Client())
req := &Request{
Query: "query { test }",
}
resp := &Response{}

err := client.MakeRequest(context.Background(), req, resp)

assert.Error(t, err)
var httpErr *HTTPError
assert.True(t, errors.As(err, &httpErr), "Error should be of type *HTTPError")
assert.Equal(t, tc.expectedStatusCode, httpErr.StatusCode)
assert.Equal(t, tc.expectedErrorBody, httpErr.Body)
})
}
}

func TestMakeRequest_Success(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
err := json.NewEncoder(w).Encode(map[string]interface{}{
"data": map[string]string{
"test": "success",
},
})
if err != nil {
t.Fatalf("Failed to encode response: %v", err)
}
}))
defer server.Close()

client := NewClient(server.URL, server.Client())
req := &Request{
Query: "query { test }",
}
resp := &Response{}

err := client.MakeRequest(context.Background(), req, resp)

assert.NoError(t, err)
assert.Equal(t, map[string]interface{}{"test": "success"}, resp.Data)
}
14 changes: 14 additions & 0 deletions graphql/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package graphql

import "fmt"

// HTTPError represents an HTTP error with status code and response body.
type HTTPError struct {
Body string
StatusCode int
}

// Error implements the error interface for HTTPError.
func (e *HTTPError) Error() string {
return fmt.Sprintf("returned error %v: %s", e.StatusCode, e.Body)
}
13 changes: 13 additions & 0 deletions internal/integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ package integration

import (
"context"
"errors"
"fmt"
"net/http"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vektah/gqlparser/v2/gqlerror"

"github.com/Khan/genqlient/graphql"
"github.com/Khan/genqlient/internal/integration/server"
Expand Down Expand Up @@ -113,6 +115,15 @@ func TestServerError(t *testing.T) {
// response -- and indeed in this case it should even have another field
// (which didn't err) set.
assert.Error(t, err)
t.Logf("Full error: %+v", err)
var gqlErrors gqlerror.List
if !assert.True(t, errors.As(err, &gqlErrors), "Error should be of type gqlerror.List") {
t.Logf("Actual error type: %T", err)
t.Logf("Error message: %v", err)
} else {
assert.Len(t, gqlErrors, 1, "Expected one GraphQL error")
assert.Equal(t, "oh no", gqlErrors[0].Message)
}
assert.NotNil(t, resp)
assert.Equal(t, "1", resp.Me.Id)
}
Expand All @@ -130,6 +141,8 @@ func TestNetworkError(t *testing.T) {
// return resp.Me.Id, err
// without a bunch of extra ceremony.
assert.Error(t, err)
var gqlErrors gqlerror.List
assert.False(t, errors.As(err, &gqlErrors), "Error should not be of type gqlerror.List for network errors")
assert.NotNil(t, resp)
assert.Equal(t, new(failingQueryResponse), resp)
}
Expand Down