Skip to content

Commit 6ccfe9d

Browse files
Nightapesalexejk
andcommitted
feat(client): Allow custom headers (#9)
Support for better options for the `Client` including custom headers. Docs & Changelog update Co-authored-by: Alexej Kubarev <[email protected]>
1 parent 7926ea3 commit 6ccfe9d

File tree

9 files changed

+297
-81
lines changed

9 files changed

+297
-81
lines changed

CHANGELOG.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,20 @@
1-
## 0.1.3 (WIP)
1+
## 0.2.x
2+
3+
## 0.2.0
24

35
Improvements:
46

5-
* User-Agent can now be configured on the Client (#6)
7+
* `NewClient` supports receiving a list of `Option`s that modify clients behavior.
8+
Initial supported options are:
9+
10+
* `HttpClient(*http.Client)` - set custom `http.Client` to be used
11+
* `Headers(map[string]string)` - set custom headers to use in every request (kudos: @Nightapes)
12+
* `UserAgent(string)` - set User-Agent identification to be used (#6). This is a shortcut for just setting `User-Agent` custom header
13+
14+
Deprecations:
15+
16+
* `NewCustomClient` is deprecated in favor of `NewClient(string, ...Option)` with `HttpClient(*http.Client)` option.
17+
This method will be removed in future versions.
618

719
## 0.1.2
820

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,11 @@ func main() {
4444
}
4545
```
4646

47-
If you want to customize any aspect of `http.Client` used to perform requests, use `NewClientWithHttpClient` instead.
48-
By default `http.DefaultClient` will be used.
47+
Customization is supported by passing a list of `Option` to the `NewClient` function.
48+
For instance:
49+
50+
- To customize any aspect of `http.Client` used to perform requests, use `HttpClient` option, otherwise `http.DefaultClient` will be used
51+
- To pass custom headers, make use of `Headers` option.
4952

5053
### Argument encoding
5154

client.go

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,38 +15,33 @@ type Client struct {
1515

1616
// NewClient creates a Client with http.DefaultClient.
1717
// If provided endpoint is not valid, an error is returned.
18-
func NewClient(endpoint string) (*Client, error) {
19-
20-
return NewClientWithHttpClient(endpoint, http.DefaultClient)
21-
}
22-
23-
// NewClientWithHttpClient allows customization of http.Client used to make RPC calls.
24-
// If provided endpoint is not valid, an error is returned.
25-
func NewClientWithHttpClient(endpoint string, httpClient *http.Client) (*Client, error) {
18+
func NewClient(endpoint string, opts ...Option) (*Client, error) {
2619

2720
// Parse Endpoint URL
2821
endpointUrl, err := url.Parse(endpoint)
2922
if err != nil {
3023
return nil, fmt.Errorf("invalid endpoint url: %w", err)
3124
}
3225

33-
codec := NewCodec(endpointUrl, httpClient)
26+
codec := NewCodec(endpointUrl, http.DefaultClient)
3427

3528
c := &Client{
3629
codec: codec,
3730
Client: rpc.NewClientWithCodec(codec),
3831
}
3932

33+
// Apply options
34+
for _, opt := range opts {
35+
opt(c)
36+
}
37+
4038
return c, nil
4139
}
4240

43-
// UserAgent returns currently configured User-Agent header that will be sent to remote server on every RPC call.
44-
func (c *Client) UserAgent() string {
45-
return c.codec.userAgent
46-
}
41+
// NewCustomClient allows customization of http.Client used to make RPC calls.
42+
// If provided endpoint is not valid, an error is returned.
43+
// Deprecated: prefer using NewClient with HttpClient Option
44+
func NewCustomClient(endpoint string, httpClient *http.Client) (*Client, error) {
4745

48-
// SetUserAgent allows customization to User-Agent header.
49-
// If set to an empty string, User-Agent header will be sent with an empty value.
50-
func (c *Client) SetUserAgent(ua string) {
51-
c.codec.userAgent = ua
46+
return NewClient(endpoint, HttpClient(httpClient))
5247
}

client_test.go

Lines changed: 11 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,17 @@ import (
1212
"github.com/stretchr/testify/assert"
1313
)
1414

15+
func TestNewClient(t *testing.T) {
16+
c, err := NewClient(":8080/rpc")
17+
18+
assert.Error(t, err)
19+
assert.Nil(t, c)
20+
21+
c, err = NewClient("http://localhost")
22+
assert.NoError(t, err)
23+
assert.NotNil(t, c)
24+
}
25+
1526
func TestClient_Call(t *testing.T) {
1627

1728
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -57,60 +68,6 @@ func TestClient_Call(t *testing.T) {
5768
assert.Equal(t, 12345, resp.Index)
5869
}
5970

60-
func TestClient_SetUserAgent(t *testing.T) {
61-
tests := []struct {
62-
name string
63-
skipSet bool
64-
agent string
65-
expect string
66-
}{
67-
{
68-
name: "default user-agent",
69-
skipSet: true,
70-
expect: defaultUserAgent,
71-
},
72-
{
73-
name: "new user-agent",
74-
agent: "my-new-agent/1.2.3",
75-
expect: "my-new-agent/1.2.3",
76-
},
77-
{
78-
name: "empty user-agent",
79-
agent: "",
80-
expect: "",
81-
},
82-
}
83-
84-
for _, tt := range tests {
85-
t.Run(tt.name, func(t *testing.T) {
86-
87-
serverCalled := false
88-
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
89-
90-
ua := r.UserAgent()
91-
92-
assert.Equal(t, tt.expect, ua)
93-
94-
serverCalled = true
95-
_, _ = fmt.Fprintln(w, string(loadTestFile(t, "response_simple.xml")))
96-
}))
97-
defer ts.Close()
98-
99-
c, err := NewClient(ts.URL)
100-
assert.NoError(t, err)
101-
102-
if !tt.skipSet {
103-
c.SetUserAgent(tt.agent)
104-
}
105-
assert.Equal(t, tt.expect, c.UserAgent())
106-
err = c.Call("test.Method", nil, nil)
107-
assert.NoError(t, err)
108-
109-
assert.True(t, serverCalled, "server must be called")
110-
})
111-
}
112-
}
113-
11471
func TestClient_Fault(t *testing.T) {
11572

11673
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

codec.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ const defaultUserAgent = "alexejk.io/go-xmlrpc"
1616
// Codec implements methods required by rpc.ClientCodec
1717
// In this implementation Codec is the one performing actual RPC requests with http.Client.
1818
type Codec struct {
19-
endpoint *url.URL
20-
httpClient *http.Client
19+
endpoint *url.URL
20+
httpClient *http.Client
21+
customHeaders map[string]string
2122

2223
mutex sync.Mutex
2324
// contains completed but not processed responses by sequence ID
@@ -46,9 +47,8 @@ func NewCodec(endpoint *url.URL, httpClient *http.Client) *Codec {
4647
return &Codec{
4748
endpoint: endpoint,
4849
httpClient: httpClient,
49-
50-
encoder: &StdEncoder{},
51-
decoder: &StdDecoder{},
50+
encoder: &StdEncoder{},
51+
decoder: &StdDecoder{},
5252

5353
pending: make(map[uint64]*rpcCall),
5454
response: nil,
@@ -72,9 +72,15 @@ func (c *Codec) WriteRequest(req *rpc.Request, args interface{}) error {
7272
}
7373

7474
httpRequest.Header.Set("Content-Type", "text/xml")
75-
httpRequest.Header.Set("Content-Length", fmt.Sprintf("%d", bodyBuffer.Len()))
7675
httpRequest.Header.Set("User-Agent", c.userAgent)
7776

77+
// Apply customer headers if set, this allows overwriting static default headers
78+
for key, value := range c.customHeaders {
79+
httpRequest.Header.Set(key, value)
80+
}
81+
82+
httpRequest.Header.Set("Content-Length", fmt.Sprintf("%d", bodyBuffer.Len()))
83+
7884
httpResponse, err := c.httpClient.Do(httpRequest)
7985
if err != nil {
8086
return err

decode_response.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,13 @@ func NewResponse(body []byte) (*Response, error) {
2121
return response, nil
2222
}
2323

24+
// ResponseParam encapsulates a nested parameter value
2425
type ResponseParam struct {
2526
Value ResponseValue `xml:"value"`
2627
}
2728

29+
// ResponseValue encapsulates one of the data types for each parameter.
30+
// Only one field should be set.
2831
type ResponseValue struct {
2932
Array []*ResponseValue `xml:"array>data>value"`
3033
Struct []*ResponseStructMember `xml:"struct>member"`
@@ -37,11 +40,13 @@ type ResponseValue struct {
3740
Base64 string `xml:"base64"`
3841
}
3942

43+
// ResponseStructMember contains name-value pair of the struct
4044
type ResponseStructMember struct {
4145
Name string `xml:"name"`
4246
Value ResponseValue `xml:"value"`
4347
}
4448

49+
// ResponseFault wraps around failure
4550
type ResponseFault struct {
4651
Value ResponseValue `xml:"value"`
4752
}

doc.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,8 @@ The simplest use-case is creating a client towards an endpoint and making calls:
1515
err = c.Call("Bugzilla.version", nil, resp)
1616
fmt.Printf("Version: %s\n", resp.BugzillaVersion.Version)
1717
18+
19+
Additional customizations, such as setting custom headers, changing User-Agent or modifying HTTP Client used to make calls,
20+
pass corresponding Options to NewClient function.
1821
*/
1922
package xmlrpc

options.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package xmlrpc
2+
3+
import "net/http"
4+
5+
// Option is a function that configures a Client by mutating it
6+
type Option func(client *Client)
7+
8+
// Headers option allows setting custom headers that will be passed with every request
9+
func Headers(headers map[string]string) Option {
10+
return func(client *Client) {
11+
client.codec.customHeaders = headers
12+
}
13+
}
14+
15+
// HttpClient option allows setting custom HTTP Client to be used for every request
16+
func HttpClient(httpClient *http.Client) Option {
17+
return func(client *Client) {
18+
client.codec.httpClient = httpClient
19+
}
20+
}
21+
22+
// UserAgent option allows setting custom User-Agent header.
23+
// This is a convenience method when only UA needs to be modified. For other cases use Headers option.
24+
func UserAgent(userAgent string) Option {
25+
return func(client *Client) {
26+
client.codec.userAgent = userAgent
27+
}
28+
}

0 commit comments

Comments
 (0)