From 4ca6aa5d7258ad21c425541e09839dee88fdd4e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Efe=20G=C3=B6kdemir?= Date: Mon, 21 Sep 2026 14:50:51 +0300 Subject: [PATCH] feat(api): expose GraphQL response headers --- pkg/api/graphql_client.go | 27 ++++++++++++++++++--------- pkg/api/graphql_client_test.go | 21 +++++++++++++++++++++ 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/pkg/api/graphql_client.go b/pkg/api/graphql_client.go index 2b12c3b..e151b78 100644 --- a/pkg/api/graphql_client.go +++ b/pkg/api/graphql_client.go @@ -66,47 +66,56 @@ func NewGraphQLClient(opts ClientOptions) (*GraphQLClient, error) { // DoWithContext executes a GraphQL query request. // The response is populated into the response argument. func (c *GraphQLClient) DoWithContext(ctx context.Context, query string, variables map[string]interface{}, response interface{}) error { + _, err := c.DoWithContextAndHeaders(ctx, query, variables, response) + return err +} + +// DoWithContextAndHeaders executes a GraphQL query request and returns the +// response headers along with any request error. The response headers are +// available even when the server returns a non-2xx status. +func (c *GraphQLClient) DoWithContextAndHeaders(ctx context.Context, query string, variables map[string]interface{}, response interface{}) (http.Header, error) { reqBody, err := json.Marshal(map[string]interface{}{"query": query, "variables": variables}) if err != nil { - return err + return nil, err } req, err := http.NewRequestWithContext(ctx, "POST", c.endpoint, bytes.NewBuffer(reqBody)) if err != nil { - return err + return nil, err } resp, err := c.httpClient.Do(req) if err != nil { - return err + return nil, err } defer resp.Body.Close() + headers := resp.Header.Clone() success := resp.StatusCode >= 200 && resp.StatusCode < 300 if !success { - return HandleHTTPError(resp) + return headers, HandleHTTPError(resp) } if resp.StatusCode == http.StatusNoContent { - return nil + return headers, nil } body, err := io.ReadAll(resp.Body) if err != nil { - return err + return headers, err } gr := graphQLResponse{Data: response} err = json.Unmarshal(body, &gr) if err != nil { - return err + return headers, err } if len(gr.Errors) > 0 { - return &GraphQLError{Errors: gr.Errors} + return headers, &GraphQLError{Errors: gr.Errors} } - return nil + return headers, nil } // Do wraps DoWithContext using context.Background. diff --git a/pkg/api/graphql_client_test.go b/pkg/api/graphql_client_test.go index 46c9db3..7f225cd 100644 --- a/pkg/api/graphql_client_test.go +++ b/pkg/api/graphql_client_test.go @@ -272,6 +272,27 @@ func TestGraphQLClientDoWithContext(t *testing.T) { } } +func TestGraphQLClientDoWithContextAndHeaders(t *testing.T) { + testutils.StubConfig(t, "") + t.Cleanup(gock.Off) + + gock.New("https://api.github.com"). + Post("/graphql"). + Reply(200). + SetHeader("X-RateLimit-Remaining", "4999"). + JSON(`{"data":{"viewer":{"login":"hubot"}}}`) + + client, err := NewGraphQLClient(ClientOptions{Host: "github.com", AuthToken: "token"}) + assert.NoError(t, err) + + res := struct{ Viewer struct{ Login string } }{} + headers, err := client.DoWithContextAndHeaders(context.Background(), "QUERY", nil, &res) + assert.NoError(t, err) + assert.Equal(t, "4999", headers.Get("X-RateLimit-Remaining")) + assert.Equal(t, "hubot", res.Viewer.Login) + assert.True(t, gock.IsDone(), printPendingMocks(gock.Pending())) +} + func TestGraphQLEndpoint(t *testing.T) { tests := []struct { name string