From fa02eafd63fefd3e5d9467745cb8b717e01c6aa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Mon, 21 Sep 2026 14:14:38 +0300 Subject: [PATCH] fix(api): normalize GraphQL HTTP errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Efe Gökdemir --- pkg/api/graphql_client.go | 46 ++++++++++++++++++++------------ pkg/api/graphql_client_test.go | 48 ++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 17 deletions(-) diff --git a/pkg/api/graphql_client.go b/pkg/api/graphql_client.go index 2b12c3b..e202bbe 100644 --- a/pkg/api/graphql_client.go +++ b/pkg/api/graphql_client.go @@ -56,8 +56,11 @@ func NewGraphQLClient(opts ClientOptions) (*GraphQLClient, error) { endpoint = swapHost(endpoint, opts.APIHost) } + graphqlHTTPClient := *httpClient + graphqlHTTPClient.Transport = graphqlErrorRoundTripper{rt: httpClient.Transport} + return &GraphQLClient{ - client: graphql.NewClient(endpoint, httpClient), + client: graphql.NewClient(endpoint, &graphqlHTTPClient), endpoint: endpoint, httpClient: httpClient, }, nil @@ -122,6 +125,10 @@ func (c *GraphQLClient) Do(query string, variables map[string]interface{}, respo // Provided input will be set as a variable named input. func (c *GraphQLClient) MutateWithContext(ctx context.Context, name string, m interface{}, variables map[string]interface{}) error { err := c.client.MutateNamed(ctx, name, m, variables) + return normalizeGraphQLError(err) +} + +func normalizeGraphQLError(err error) error { var graphQLErrs graphql.Errors if err != nil && errors.As(err, &graphQLErrs) { items := make([]GraphQLErrorItem, len(graphQLErrs)) @@ -134,8 +141,14 @@ func (c *GraphQLClient) MutateWithContext(ctx context.Context, name string, m in Type: e.Type, } } - err = &GraphQLError{items} + return &GraphQLError{items} } + + var httpErr *HTTPError + if err != nil && errors.As(err, &httpErr) { + return httpErr + } + return err } @@ -151,21 +164,7 @@ func (c *GraphQLClient) Mutate(name string, m interface{}, variables map[string] // to the GitHub GraphQL schema. func (c *GraphQLClient) QueryWithContext(ctx context.Context, name string, q interface{}, variables map[string]interface{}) error { err := c.client.QueryNamed(ctx, name, q, variables) - var graphQLErrs graphql.Errors - if err != nil && errors.As(err, &graphQLErrs) { - items := make([]GraphQLErrorItem, len(graphQLErrs)) - for i, e := range graphQLErrs { - items[i] = GraphQLErrorItem{ - Message: e.Message, - Locations: e.Locations, - Path: e.Path, - Extensions: e.Extensions, - Type: e.Type, - } - } - err = &GraphQLError{items} - } - return err + return normalizeGraphQLError(err) } // Query wraps QueryWithContext using context.Background. @@ -178,6 +177,19 @@ type graphQLResponse struct { Errors []GraphQLErrorItem } +type graphqlErrorRoundTripper struct { + rt http.RoundTripper +} + +func (rt graphqlErrorRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + resp, err := rt.rt.RoundTrip(req) + if err != nil || resp.StatusCode >= 200 && resp.StatusCode < 300 { + return resp, err + } + + return resp, HandleHTTPError(resp) +} + func graphQLEndpoint(host string) string { if isGarage(host) { return fmt.Sprintf("https://%s/api/graphql", host) diff --git a/pkg/api/graphql_client_test.go b/pkg/api/graphql_client_test.go index 46c9db3..3680a66 100644 --- a/pkg/api/graphql_client_test.go +++ b/pkg/api/graphql_client_test.go @@ -103,6 +103,54 @@ func TestGraphQLClientMutateError(t *testing.T) { assert.True(t, gock.IsDone(), printPendingMocks(gock.Pending())) } +func TestGraphQLClientQueryHTTPError(t *testing.T) { + testutils.StubConfig(t, testConfig()) + t.Cleanup(gock.Off) + + gock.New("https://api.github.com"). + Post("/graphql"). + MatchHeader("Authorization", "token abc123"). + Reply(http.StatusUnauthorized). + JSON(`{"message":"Bad credentials"}`) + + client, err := DefaultGraphQLClient() + assert.NoError(t, err) + + var res struct{ Viewer struct{ Login string } } + err = client.Query("QUERY", &res, nil) + var httpErr *HTTPError + assert.ErrorAs(t, err, &httpErr) + assert.Equal(t, http.StatusUnauthorized, httpErr.StatusCode) + assert.Equal(t, "Bad credentials", httpErr.Message) + assert.EqualError(t, err, "HTTP 401: Bad credentials (https://api.github.com/graphql)") + assert.True(t, gock.IsDone(), printPendingMocks(gock.Pending())) +} + +func TestGraphQLClientMutateHTTPError(t *testing.T) { + testutils.StubConfig(t, testConfig()) + t.Cleanup(gock.Off) + + gock.New("https://api.github.com"). + Post("/graphql"). + MatchHeader("Authorization", "token abc123"). + Reply(http.StatusForbidden). + JSON(`{"message":"Forbidden"}`) + + client, err := DefaultGraphQLClient() + assert.NoError(t, err) + + var mutation struct { + UpdateRepository struct{ Repository struct{ Name string } } + } + err = client.Mutate("MUTATE", &mutation, nil) + var httpErr *HTTPError + assert.ErrorAs(t, err, &httpErr) + assert.Equal(t, http.StatusForbidden, httpErr.StatusCode) + assert.Equal(t, "Forbidden", httpErr.Message) + assert.EqualError(t, err, "HTTP 403: Forbidden (https://api.github.com/graphql)") + assert.True(t, gock.IsDone(), printPendingMocks(gock.Pending())) +} + func TestGraphQLClientDo(t *testing.T) { testutils.StubConfig(t, "")