Skip to content

Commit

Permalink
feat: add an option to allow binding response headers
Browse files Browse the repository at this point in the history
  • Loading branch information
hgiasac committed Feb 14, 2025
1 parent 47ee315 commit 89a6861
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 13 deletions.
29 changes: 24 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ For more information, see package [`github.com/shurcooL/githubv4`](https://githu
- [Installation](#installation)
- [Usage](#usage)
- [Authentication](#authentication)
- [WithRequestModifier](#withrequestmodifier)
- [OAuth2](#oauth2)
- [Simple Query](#simple-query)
- [Arguments and Variables](#arguments-and-variables)
- [Custom scalar tag](#custom-scalar-tag)
Expand All @@ -37,6 +39,7 @@ For more information, see package [`github.com/shurcooL/githubv4`](https://githu
- [Custom WebSocket client](#custom-websocket-client)
- [Options](#options-1)
- [Execute pre-built query](#execute-pre-built-query)
- [Get extensions from response](#get-extensions-from-response)
- [With operation name (deprecated)](#with-operation-name-deprecated)
- [Raw bytes response](#raw-bytes-response)
- [Multiple mutations with ordered map](#multiple-mutations-with-ordered-map)
Expand Down Expand Up @@ -67,7 +70,22 @@ client := graphql.NewClient("https://example.com/graphql", nil)

### Authentication

Some GraphQL servers may require authentication. The `graphql` package does not directly handle authentication. Instead, when creating a new client, you're expected to pass an `http.Client` that performs authentication. The easiest and recommended way to do this is to use the [`golang.org/x/oauth2`](https://golang.org/x/oauth2) package. You'll need an OAuth token with the right scopes. Then:
Some GraphQL servers may require authentication. The `graphql` package does not directly handle authentication. Instead, when creating a new client, you're expected to pass an `http.Client` that performs authentication.

#### WithRequestModifier

Use `WithRequestModifier` method to inject headers into the request before sending to the GraphQL server.

```go
client := graphql.NewClient(endpoint, http.DefaultClient).
WithRequestModifier(func(r *http.Request) {
r.Header.Set("Authorization", "random-token")
})
```

#### OAuth2

The easiest and recommended way to do this is to use the [`golang.org/x/oauth2`](https://golang.org/x/oauth2) package. You'll need an OAuth token with the right scopes. Then:

```Go
import "golang.org/x/oauth2"
Expand Down Expand Up @@ -736,6 +754,7 @@ client.Query(ctx context.Context, q interface{}, variables map[string]interface{
```
Currently, there are 3 option types:
- `operation_name`
- `operation_directive`
- `bind_extensions`
Expand Down Expand Up @@ -982,11 +1001,11 @@ Because the GraphQL query string is generated in runtime using reflection, it is
## Directories
| Path | Synopsis |
| -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| [example/graphqldev](https://godoc.org/github.com/shurcooL/graphql/example/graphqldev) | graphqldev is a test program currently being used for developing graphql package. |
| Path | Synopsis |
| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| [example/graphqldev](https://godoc.org/github.com/shurcooL/graphql/example/graphqldev) | graphqldev is a test program currently being used for developing graphql package. |
| [ident](https://godoc.org/github.com/shurcooL/graphql/ident) | Package ident provides functions for parsing and converting identifier names between various naming conventions. |
| [internal/jsonutil](https://godoc.org/github.com/shurcooL/graphql/internal/jsonutil) | Package jsonutil provides a function for decoding JSON into a GraphQL query data structure. |
| [internal/jsonutil](https://godoc.org/github.com/shurcooL/graphql/internal/jsonutil) | Package jsonutil provides a function for decoding JSON into a GraphQL query data structure. |
## References
Expand Down
11 changes: 11 additions & 0 deletions graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,20 @@ func (c *Client) request(ctx context.Context, query string, variables map[string
if c.debug {
e = e.withRequest(request, reqReader)
}

return nil, nil, nil, nil, Errors{e}
}

defer resp.Body.Close()

if options != nil && options.headers != nil {
for key, values := range resp.Header {
for _, value := range values {
options.headers.Add(key, value)
}
}
}

r := resp.Body

if resp.Header.Get("Content-Encoding") == "gzip" {
Expand Down Expand Up @@ -350,6 +360,7 @@ func (c *Client) WithRequestModifier(f RequestModifier) *Client {
url: c.url,
httpClient: c.httpClient,
requestModifier: f,
debug: c.debug,
}
}

Expand Down
26 changes: 24 additions & 2 deletions graphql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,8 @@ func TestClient_BindExtensions(t *testing.T) {
t.Fatalf("got q.User.Name: %q, want: %q", got, want)
}

err = client.Query(context.Background(), &q, map[string]interface{}{}, graphql.BindExtensions(&ext))
headers := http.Header{}
err = client.Query(context.Background(), &q, map[string]interface{}{}, graphql.BindExtensions(&ext), graphql.BindResponseHeaders(&headers))
if err != nil {
t.Fatal(err)
}
Expand All @@ -518,18 +519,30 @@ func TestClient_BindExtensions(t *testing.T) {
if got, want := ext.Domain, "users"; got != want {
t.Errorf("got ext.Domain: %q, want: %q", got, want)
}

if len(headers) != 1 {
t.Error("got empty headers, want 1")
}

if got, want := headers.Get("content-type"), "application/json"; got != want {
t.Errorf("got headers[content-type]: %q, want: %s", got, want)
}
}

// Test exec pre-built query, return raw json string and map
// with extensions
func TestClient_Exec_QueryRawWithExtensions(t *testing.T) {
testResponseHeader := "X-Test-Response"
testResponseHeaderValue := "graphql"

mux := http.NewServeMux()
mux.HandleFunc("/graphql", func(w http.ResponseWriter, req *http.Request) {
body := mustRead(req.Body)
if got, want := body, `{"query":"{user{id,name}}"}`+"\n"; got != want {
t.Errorf("got body: %v, want %v", got, want)
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set(testResponseHeader, testResponseHeaderValue)
mustWrite(w, `{"data": {"user": {"name": "Gopher"}}, "extensions": {"id": 1, "domain": "users"}}`)
})
client := graphql.NewClient("/graphql", &http.Client{Transport: localRoundTripper{handler: mux}})
Expand All @@ -539,7 +552,8 @@ func TestClient_Exec_QueryRawWithExtensions(t *testing.T) {
Domain string `json:"domain"`
}

_, extensions, err := client.ExecRawWithExtensions(context.Background(), "{user{id,name}}", map[string]interface{}{})
headers := http.Header{}
_, extensions, err := client.ExecRawWithExtensions(context.Background(), "{user{id,name}}", map[string]interface{}{}, graphql.BindResponseHeaders(&headers))
if err != nil {
t.Fatal(err)
}
Expand All @@ -559,6 +573,14 @@ func TestClient_Exec_QueryRawWithExtensions(t *testing.T) {
if got, want := ext.Domain, "users"; got != want {
t.Errorf("got ext.Domain: %q, want: %q", got, want)
}

if len(headers) != 2 {
t.Error("got empty headers, want 2")
}

if headerValue := headers.Get(testResponseHeader); headerValue != testResponseHeaderValue {
t.Errorf("got headers[%s]: %q, want: %s", testResponseHeader, headerValue, testResponseHeaderValue)
}
}

// localRoundTripper is an http.RoundTripper that executes HTTP transactions
Expand Down
16 changes: 16 additions & 0 deletions option.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package graphql

import "net/http"

// OptionType represents the logic of graphql query construction
type OptionType string

Expand Down Expand Up @@ -46,3 +48,17 @@ func (ono bindExtensionsOption) Type() OptionType {
func BindExtensions(value any) Option {
return bindExtensionsOption{value: value}
}

// bind the struct pointer to return headers from response
type bindResponseHeadersOption struct {
value *http.Header
}

func (ono bindResponseHeadersOption) Type() OptionType {
return "bind_extensions"
}

// BindExtensionsBindResponseHeaders bind the header response to the pointer
func BindResponseHeaders(value *http.Header) Option {
return bindResponseHeadersOption{value: value}
}
4 changes: 4 additions & 0 deletions query.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"io"
"net/http"
"reflect"
"sort"
"strconv"
Expand All @@ -17,6 +18,7 @@ type constructOptionsOutput struct {
operationName string
operationDirectives []string
extensions any
headers *http.Header
}

func (coo constructOptionsOutput) OperationDirectivesString() string {
Expand All @@ -36,6 +38,8 @@ func constructOptions(options []Option) (*constructOptionsOutput, error) {
output.operationName = opt.name
case bindExtensionsOption:
output.extensions = opt.value
case bindResponseHeadersOption:
output.headers = opt.value
default:
if opt.Type() != OptionTypeOperationDirective {
return nil, fmt.Errorf("invalid query option type: %s", option.Type())
Expand Down
10 changes: 4 additions & 6 deletions subscription_graphql_ws_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,10 @@ type user_insert_input map[string]interface{}

func hasura_setupClients(protocol SubscriptionProtocolType) (*Client, *SubscriptionClient) {
endpoint := fmt.Sprintf("%s/v1/graphql", hasuraTestHost)
client := NewClient(endpoint, &http.Client{Transport: headerRoundTripper{
setHeaders: func(req *http.Request) {
req.Header.Set("x-hasura-admin-secret", hasuraTestAdminSecret)
},
rt: http.DefaultTransport,
}})
client := NewClient(endpoint, http.DefaultClient).
WithRequestModifier(func(r *http.Request) {
r.Header.Set("x-hasura-admin-secret", hasuraTestAdminSecret)
})

subscriptionClient := NewSubscriptionClient(endpoint).
WithProtocol(protocol).
Expand Down

0 comments on commit 89a6861

Please sign in to comment.