Skip to content

Commit

Permalink
Merge pull request gojek#27 from darshanime/custom_http_client
Browse files Browse the repository at this point in the history
allow custom http client
  • Loading branch information
rShetty authored Apr 6, 2018
2 parents b496d1f + cd0b79b commit badd722
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 2 deletions.
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,40 @@ client.SetRetryCount(4)
// The rest is the same as the first example
```

### Custom HTTP clients

Heimdall supports custom HTTP clients. This is useful if you are using a client imported from another library and/or wish to implement custom logging, cookies, headers etc for each request that you make with your client.

Under the hood, the `httpClient` struct now accepts `Doer`, which is the standard interface implemented by HTTP clients (including the standard library's `net/*http.Client`)

Let's say we wish to add authorization headers to all our requests.

We can define our client `myHTTPClient`

```go
type myHTTPClient struct {
client http.Client
}

func (c *myHTTPClient) Do(request *http.Request) (*http.Response, error) {
request.SetBasicAuth("username", "passwd")
return c.client.Do(request)
}
```

And set this with `httpClient.SetCustomHTTPClient(&myHTTPClient{client: http.DefaultClient})`

Now, each sent request will have the `Authorization` header to use HTTP basic authentication with the provided username and password.

This can be done for the hystrix client as well

```
hystrixClient.SetCustomHTTPClient(&myHTTPClient{
client: http.Client{Timeout: 25 * time.Millisecond}})
// The rest is the same as the first example
```

## Documentation

Further documentation can be found on [godoc.org](https://www.godoc.org/github.com/gojektech/heimdall)
Expand Down
7 changes: 7 additions & 0 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import (
"net/http"
)

// Doer interface has the method required to use a type as custom http client.
// The net/*http.Client type satisfies this interface.
type Doer interface {
Do(*http.Request) (*http.Response, error)
}

// Client Is a generic HTTP client interface
type Client interface {
Get(url string, headers http.Header) (*http.Response, error)
Expand All @@ -16,4 +22,5 @@ type Client interface {

SetRetryCount(count int)
SetRetrier(retrier Retriable)
SetCustomHTTPClient(customHTTPClient Doer)
}
71 changes: 71 additions & 0 deletions examples/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,74 @@ func hystrixClientUsage() error {
fmt.Printf("Response: %s", string(respBody))
return nil
}

type myHTTPClient struct {
client http.Client
}

func (c *myHTTPClient) Do(request *http.Request) (*http.Response, error) {
request.SetBasicAuth("username", "passwd")
return c.client.Do(request)
}

func customHTTPClientUsage() error {
httpClient := heimdall.NewHTTPClient(0 * time.Millisecond)

// replace with custom HTTP client
httpClient.SetCustomHTTPClient(&myHTTPClient{
client: http.Client{Timeout: 25 * time.Millisecond}})

headers := http.Header{}
headers.Set("Content-Type", "application/json")

httpClient.SetRetryCount(2)
httpClient.SetRetrier(heimdall.NewRetrier(heimdall.NewConstantBackoff(10, 5)))

response, err := httpClient.Get(baseURL, headers)
if err != nil {
return errors.Wrap(err, "failed to make a request to server")
}

defer response.Body.Close()

respBody, err := ioutil.ReadAll(response.Body)
if err != nil {
return errors.Wrap(err, "failed to read response body")
}

fmt.Printf("Response: %s", string(respBody))
return nil
}

func customHystrixClientUsage() error {
timeout := 0 * time.Millisecond

hystrixConfig := heimdall.NewHystrixConfig("MyCommand", heimdall.HystrixCommandConfig{
Timeout: 1100,
MaxConcurrentRequests: 100,
ErrorPercentThreshold: 25,
SleepWindow: 10,
RequestVolumeThreshold: 10,
})

hystrixClient := heimdall.NewHystrixHTTPClient(timeout, hystrixConfig)

hystrixClient.SetCustomHTTPClient(&myHTTPClient{
client: http.Client{Timeout: 25 * time.Millisecond}})

headers := http.Header{}
response, err := hystrixClient.Get(baseURL, headers)
if err != nil {
return errors.Wrap(err, "failed to make a request to server")
}

defer response.Body.Close()

respBody, err := ioutil.ReadAll(response.Body)
if err != nil {
return errors.Wrap(err, "failed to read response body")
}

fmt.Printf("Response: %s", string(respBody))
return nil
}
7 changes: 6 additions & 1 deletion http_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
const defaultRetryCount int = 0

type httpClient struct {
client *http.Client
client Doer

retryCount int
retrier Retriable
Expand All @@ -36,6 +36,11 @@ func (c *httpClient) SetRetryCount(count int) {
c.retryCount = count
}

// SetCustomHTTPClient sets custom HTTP client
func (c *httpClient) SetCustomHTTPClient(customHTTPClient Doer) {
c.client = customHTTPClient
}

// SetRetrier sets the strategy for retrying
func (c *httpClient) SetRetrier(retrier Retriable) {
c.retrier = retrier
Expand Down
35 changes: 35 additions & 0 deletions http_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,41 @@ func TestHTTPClientGetReturnsErrorOn5xxFailure(t *testing.T) {
assert.Equal(t, "server error: 500", err.Error())
}

type myHTTPClient struct {
client http.Client
}

func (c *myHTTPClient) Do(request *http.Request) (*http.Response, error) {
request.Header.Set("foo", "bar")
return c.client.Do(request)
}

func TestCustomHTTPClientHeaderSuccess(t *testing.T) {
client := NewHTTPClient(10 * time.Millisecond)

client.SetCustomHTTPClient(&myHTTPClient{
client: http.Client{Timeout: 25 * time.Millisecond}})

dummyHandler := func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, r.Header.Get("foo"), "bar")
assert.NotEqual(t, r.Header.Get("foo"), "baz")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{ "response": "ok" }`))
}

server := httptest.NewServer(http.HandlerFunc(dummyHandler))
defer server.Close()

req, err := http.NewRequest(http.MethodGet, server.URL, nil)
require.NoError(t, err)
response, err := client.Do(req)
assert.Equal(t, http.StatusOK, response.StatusCode)

body, err := ioutil.ReadAll(response.Body)
require.NoError(t, err)
assert.Equal(t, "{ \"response\": \"ok\" }", string(body))
}

func respBody(t *testing.T, response *http.Response) string {
if response.Body != nil {
defer response.Body.Close()
Expand Down
7 changes: 6 additions & 1 deletion hystrix_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
const defaultHystrixRetryCount int = 0

type hystrixHTTPClient struct {
client *http.Client
client Doer

hystrixCommandName string

Expand Down Expand Up @@ -51,6 +51,11 @@ func (hhc *hystrixHTTPClient) SetRetrier(retrier Retriable) {
hhc.retrier = retrier
}

// SetRetrier sets the strategy for retrying
func (hhc *hystrixHTTPClient) SetCustomHTTPClient(customHTTPClient Doer) {
hhc.client = customHTTPClient
}

// Get makes a HTTP GET request to provided URL
func (hhc *hystrixHTTPClient) Get(url string, headers http.Header) (*http.Response, error) {
var response *http.Response
Expand Down
39 changes: 39 additions & 0 deletions hystrix_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -407,3 +407,42 @@ func TestHystrixHTTPClientReturnsFallbackFailureWithAFallBackFunctionWhichReturn
_, err := client.Get("http://foobar.example", http.Header{})
assert.Nil(t, err)
}

func TestCustomHystrixHTTPClientDoSuccess(t *testing.T) {
hystrixCommandConfig := hystrix.CommandConfig{
Timeout: 10,
MaxConcurrentRequests: 100,
ErrorPercentThreshold: 10,
SleepWindow: 100,
RequestVolumeThreshold: 10,
}

timeout := 10 * time.Millisecond

client := NewHystrixHTTPClient(timeout, HystrixConfig{
commandName: "some_new_command_name",
commandConfig: hystrixCommandConfig,
})

client.SetCustomHTTPClient(&myHTTPClient{
client: http.Client{Timeout: 25 * time.Millisecond}})

dummyHandler := func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, r.Header.Get("foo"), "bar")
assert.NotEqual(t, r.Header.Get("foo"), "baz")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{ "response": "ok" }`))
}

server := httptest.NewServer(http.HandlerFunc(dummyHandler))
defer server.Close()

req, err := http.NewRequest(http.MethodGet, server.URL, nil)
require.NoError(t, err)
response, err := client.Do(req)
assert.Equal(t, http.StatusOK, response.StatusCode)

body, err := ioutil.ReadAll(response.Body)
require.NoError(t, err)
assert.Equal(t, "{ \"response\": \"ok\" }", string(body))
}

0 comments on commit badd722

Please sign in to comment.