Skip to content

Commit 3b34c44

Browse files
authored
feat: introduce BindResponseHeaders option (#163)
* add BindResponseHeaders option to allow binding response headers
1 parent 47ee315 commit 3b34c44

File tree

6 files changed

+101
-24
lines changed

6 files changed

+101
-24
lines changed

README.md

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ For more information, see package [`github.com/shurcooL/githubv4`](https://githu
1616
- [Installation](#installation)
1717
- [Usage](#usage)
1818
- [Authentication](#authentication)
19+
- [WithRequestModifier](#withrequestmodifier)
20+
- [OAuth2](#oauth2)
1921
- [Simple Query](#simple-query)
2022
- [Arguments and Variables](#arguments-and-variables)
2123
- [Custom scalar tag](#custom-scalar-tag)
@@ -37,6 +39,8 @@ For more information, see package [`github.com/shurcooL/githubv4`](https://githu
3739
- [Custom WebSocket client](#custom-websocket-client)
3840
- [Options](#options-1)
3941
- [Execute pre-built query](#execute-pre-built-query)
42+
- [Get extensions from response](#get-extensions-from-response)
43+
- [Get headers from response](#get-headers-from-response)
4044
- [With operation name (deprecated)](#with-operation-name-deprecated)
4145
- [Raw bytes response](#raw-bytes-response)
4246
- [Multiple mutations with ordered map](#multiple-mutations-with-ordered-map)
@@ -67,7 +71,22 @@ client := graphql.NewClient("https://example.com/graphql", nil)
6771

6872
### Authentication
6973

70-
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:
74+
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.
75+
76+
#### WithRequestModifier
77+
78+
Use `WithRequestModifier` method to inject headers into the request before sending to the GraphQL server.
79+
80+
```go
81+
client := graphql.NewClient(endpoint, http.DefaultClient).
82+
WithRequestModifier(func(r *http.Request) {
83+
r.Header.Set("Authorization", "random-token")
84+
})
85+
```
86+
87+
#### OAuth2
88+
89+
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:
7190

7291
```Go
7392
import "golang.org/x/oauth2"
@@ -735,10 +754,12 @@ type Option interface {
735754
client.Query(ctx context.Context, q interface{}, variables map[string]interface{}, options ...Option) error
736755
```
737756
738-
Currently, there are 3 option types:
757+
Currently, there are 4 option types:
758+
739759
- `operation_name`
740760
- `operation_directive`
741761
- `bind_extensions`
762+
- `bind_response_headers`
742763
743764
The operation name option is built-in because it is unique. We can use the option directly with `OperationName`.
744765
@@ -863,6 +884,21 @@ if err != nil {
863884
fmt.Println("Extensions:", extensions)
864885
```
865886
887+
### Get headers from response
888+
889+
Use the `BindResponseHeaders` option to bind headers from the response.
890+
891+
```go
892+
headers := http.Header{}
893+
err := client.Query(context.TODO(), &q, map[string]any{}, graphql.BindResponseHeaders(&headers))
894+
if err != nil {
895+
panic(err)
896+
}
897+
898+
fmt.Println(headers.Get("content-type"))
899+
// application/json
900+
```
901+
866902
### With operation name (deprecated)
867903
868904
```Go
@@ -982,11 +1018,11 @@ Because the GraphQL query string is generated in runtime using reflection, it is
9821018
9831019
## Directories
9841020
985-
| Path | Synopsis |
986-
| -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
987-
| [example/graphqldev](https://godoc.org/github.com/shurcooL/graphql/example/graphqldev) | graphqldev is a test program currently being used for developing graphql package. |
1021+
| Path | Synopsis |
1022+
| -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
1023+
| [example/graphqldev](https://godoc.org/github.com/shurcooL/graphql/example/graphqldev) | graphqldev is a test program currently being used for developing graphql package. |
9881024
| [ident](https://godoc.org/github.com/shurcooL/graphql/ident) | Package ident provides functions for parsing and converting identifier names between various naming conventions. |
989-
| [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. |
1025+
| [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. |
9901026
9911027
## References
9921028

graphql.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,20 @@ func (c *Client) request(ctx context.Context, query string, variables map[string
163163
if c.debug {
164164
e = e.withRequest(request, reqReader)
165165
}
166+
166167
return nil, nil, nil, nil, Errors{e}
167168
}
169+
168170
defer resp.Body.Close()
169171

172+
if options != nil && options.headers != nil {
173+
for key, values := range resp.Header {
174+
for _, value := range values {
175+
options.headers.Add(key, value)
176+
}
177+
}
178+
}
179+
170180
r := resp.Body
171181

172182
if resp.Header.Get("Content-Encoding") == "gzip" {
@@ -350,6 +360,7 @@ func (c *Client) WithRequestModifier(f RequestModifier) *Client {
350360
url: c.url,
351361
httpClient: c.httpClient,
352362
requestModifier: f,
363+
debug: c.debug,
353364
}
354365
}
355366

graphql_test.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -503,7 +503,8 @@ func TestClient_BindExtensions(t *testing.T) {
503503
t.Fatalf("got q.User.Name: %q, want: %q", got, want)
504504
}
505505

506-
err = client.Query(context.Background(), &q, map[string]interface{}{}, graphql.BindExtensions(&ext))
506+
headers := http.Header{}
507+
err = client.Query(context.Background(), &q, map[string]interface{}{}, graphql.BindExtensions(&ext), graphql.BindResponseHeaders(&headers))
507508
if err != nil {
508509
t.Fatal(err)
509510
}
@@ -518,18 +519,30 @@ func TestClient_BindExtensions(t *testing.T) {
518519
if got, want := ext.Domain, "users"; got != want {
519520
t.Errorf("got ext.Domain: %q, want: %q", got, want)
520521
}
522+
523+
if len(headers) != 1 {
524+
t.Error("got empty headers, want 1")
525+
}
526+
527+
if got, want := headers.Get("content-type"), "application/json"; got != want {
528+
t.Errorf("got headers[content-type]: %q, want: %s", got, want)
529+
}
521530
}
522531

523532
// Test exec pre-built query, return raw json string and map
524533
// with extensions
525534
func TestClient_Exec_QueryRawWithExtensions(t *testing.T) {
535+
testResponseHeader := "X-Test-Response"
536+
testResponseHeaderValue := "graphql"
537+
526538
mux := http.NewServeMux()
527539
mux.HandleFunc("/graphql", func(w http.ResponseWriter, req *http.Request) {
528540
body := mustRead(req.Body)
529541
if got, want := body, `{"query":"{user{id,name}}"}`+"\n"; got != want {
530542
t.Errorf("got body: %v, want %v", got, want)
531543
}
532544
w.Header().Set("Content-Type", "application/json")
545+
w.Header().Set(testResponseHeader, testResponseHeaderValue)
533546
mustWrite(w, `{"data": {"user": {"name": "Gopher"}}, "extensions": {"id": 1, "domain": "users"}}`)
534547
})
535548
client := graphql.NewClient("/graphql", &http.Client{Transport: localRoundTripper{handler: mux}})
@@ -539,7 +552,8 @@ func TestClient_Exec_QueryRawWithExtensions(t *testing.T) {
539552
Domain string `json:"domain"`
540553
}
541554

542-
_, extensions, err := client.ExecRawWithExtensions(context.Background(), "{user{id,name}}", map[string]interface{}{})
555+
headers := http.Header{}
556+
_, extensions, err := client.ExecRawWithExtensions(context.Background(), "{user{id,name}}", map[string]interface{}{}, graphql.BindResponseHeaders(&headers))
543557
if err != nil {
544558
t.Fatal(err)
545559
}
@@ -559,6 +573,14 @@ func TestClient_Exec_QueryRawWithExtensions(t *testing.T) {
559573
if got, want := ext.Domain, "users"; got != want {
560574
t.Errorf("got ext.Domain: %q, want: %q", got, want)
561575
}
576+
577+
if len(headers) != 2 {
578+
t.Error("got empty headers, want 2")
579+
}
580+
581+
if headerValue := headers.Get(testResponseHeader); headerValue != testResponseHeaderValue {
582+
t.Errorf("got headers[%s]: %q, want: %s", testResponseHeader, headerValue, testResponseHeaderValue)
583+
}
562584
}
563585

564586
// localRoundTripper is an http.RoundTripper that executes HTTP transactions

option.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package graphql
22

3+
import "net/http"
4+
35
// OptionType represents the logic of graphql query construction
46
type OptionType string
57

@@ -46,3 +48,17 @@ func (ono bindExtensionsOption) Type() OptionType {
4648
func BindExtensions(value any) Option {
4749
return bindExtensionsOption{value: value}
4850
}
51+
52+
// bind the struct pointer to return headers from response
53+
type bindResponseHeadersOption struct {
54+
value *http.Header
55+
}
56+
57+
func (ono bindResponseHeadersOption) Type() OptionType {
58+
return "bind_response_headers"
59+
}
60+
61+
// BindExtensionsBindResponseHeaders bind the header response to the pointer
62+
func BindResponseHeaders(value *http.Header) Option {
63+
return bindResponseHeadersOption{value: value}
64+
}

query.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"fmt"
77
"io"
8+
"net/http"
89
"reflect"
910
"sort"
1011
"strconv"
@@ -17,6 +18,7 @@ type constructOptionsOutput struct {
1718
operationName string
1819
operationDirectives []string
1920
extensions any
21+
headers *http.Header
2022
}
2123

2224
func (coo constructOptionsOutput) OperationDirectivesString() string {
@@ -36,6 +38,8 @@ func constructOptions(options []Option) (*constructOptionsOutput, error) {
3638
output.operationName = opt.name
3739
case bindExtensionsOption:
3840
output.extensions = opt.value
41+
case bindResponseHeadersOption:
42+
output.headers = opt.value
3943
default:
4044
if opt.Type() != OptionTypeOperationDirective {
4145
return nil, fmt.Errorf("invalid query option type: %s", option.Type())

subscription_graphql_ws_test.go

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,14 @@ const (
2020
hasuraTestAdminSecret = "hasura"
2121
)
2222

23-
type headerRoundTripper struct {
24-
setHeaders func(req *http.Request)
25-
rt http.RoundTripper
26-
}
27-
28-
func (h headerRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
29-
h.setHeaders(req)
30-
return h.rt.RoundTrip(req)
31-
}
32-
3323
type user_insert_input map[string]interface{}
3424

3525
func hasura_setupClients(protocol SubscriptionProtocolType) (*Client, *SubscriptionClient) {
3626
endpoint := fmt.Sprintf("%s/v1/graphql", hasuraTestHost)
37-
client := NewClient(endpoint, &http.Client{Transport: headerRoundTripper{
38-
setHeaders: func(req *http.Request) {
39-
req.Header.Set("x-hasura-admin-secret", hasuraTestAdminSecret)
40-
},
41-
rt: http.DefaultTransport,
42-
}})
27+
client := NewClient(endpoint, http.DefaultClient).
28+
WithRequestModifier(func(r *http.Request) {
29+
r.Header.Set("x-hasura-admin-secret", hasuraTestAdminSecret)
30+
})
4331

4432
subscriptionClient := NewSubscriptionClient(endpoint).
4533
WithProtocol(protocol).

0 commit comments

Comments
 (0)