Repository: go-resty/resty
Pull request #413 (2021-02-10 08:09:24)

Added a way set a Non-Canonical Header
Some API require use of Non-Canonical Header fields.

codecov[bot]: # [Codecov](https://codecov.io/gh/go-resty/resty/pull/413?src=pr&el=h1) Report
> Merging [#413](https://codecov.io/gh/go-resty/resty/pull/413?src=pr&el=desc) (53f0259) into [master](https://codecov.io/gh/go-resty/resty/commit/61f4307870842f056ac439c0d627a9611846de02?el=desc) (61f4307) will **decrease** coverage by `0.18%`.
> The diff coverage is `0.00%`.

[![Impacted file tree graph](https://codecov.io/gh/go-resty/resty/pull/413/graphs/tree.svg?width=650&height=150&src=pr&token=9t4RCu3fVO)](https://codecov.io/gh/go-resty/resty/pull/413?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #413      +/-   ##
==========================================
- Coverage   95.97%   95.78%   -0.19%     
==========================================
  Files          10       10              
  Lines        1019     1021       +2     
==========================================
  Hits          978      978              
- Misses         23       25       +2     
  Partials       18       18              
```


| [Impacted Files](https://codecov.io/gh/go-resty/resty/pull/413?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [request.go](https://codecov.io/gh/go-resty/resty/pull/413/diff?src=pr&el=tree#diff-cmVxdWVzdC5nbw==) | `99.02% <0.00%> (-0.98%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/go-resty/resty/pull/413?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/go-resty/resty/pull/413?src=pr&el=footer). Last update [61f4307...53f0259](https://codecov.io/gh/go-resty/resty/pull/413?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

moorereason: I don't like this API.  Just set the map values manually and move on:

```
r := client.R()
r.Header["My-WHATEVER"] = "foo"
```
Lao-Tuo: > I don't like this API. Just set the map values manually and move on:
> 
> ```
> r := client.R()
> r.Header["My-WHATEVER"] = "foo"
> ```

That should be:
r := client.R()
r.Header["My-WHATEVER"]  = []string{"foo"}

but sure, you can do that..

However, that would be the same with setting normal headers, too.. you can just skip the wrapping.
r := client.R()
r.Header.Set("My-WHATEVER","foo")

why do we have a "SetHeader" in the first place?
moorereason: > why do we have a "SetHeader" in the first place?

To make the common case chainable with the API.

However, I'm softening to the idea of expanding the API for this.  I may have been in a cantankerous mood yesterday.
Lao-Tuo: > To make the common case chainable with the API.

And that was the intention behind this one as well. ;)
The actual usecase that came up for me is the Cisco DNAC API (which needs non-canonical, lowercase Header-fields):
`
	response, err := RestyClient.R().
		SetHeaderVerbatim("entity_type", getClientEnrichmentDetailsQueryParams.Type).
		SetHeaderVerbatim("entity_value", getClientEnrichmentDetailsQueryParams.Value).
		SetResult(&GetClientEnrichmentDetailsResponse{}).
		SetError(&Error{}).
		Get(path)
`


Patch:
diff --git a/client.go b/client.go
index 4e54f69..8766389 100644
--- a/client.go
+++ b/client.go
@@ -108,6 +108,7 @@ type Client struct {
 	RetryWaitTime         time.Duration
 	RetryMaxWaitTime      time.Duration
 	RetryConditions       []RetryConditionFunc
+	RetryHooks            []OnRetryFunc
 	RetryAfter            RetryAfterFunc
 	JSONMarshal           func(v interface{}) ([]byte, error)
 	JSONUnmarshal         func(data []byte, v interface{}) error
@@ -193,6 +194,19 @@ func (c *Client) SetHeaders(headers map[string]string) *Client {
 	return c
 }
 
+// SetHeaderVerbatim method is to set a single header field and its value verbatim in the current request.
+//
+// For Example: To set `all_lowercase` and `UPPERCASE` as `available`.
+// 		client.R().
+//			SetHeaderVerbatim("all_lowercase", "available").
+//			SetHeaderVerbatim("UPPERCASE", "available")
+//
+// Also you can override header value, which was set at client instance level.
+func (c *Client) SetHeaderVerbatim(header, value string) *Client {
+	c.Header[header] = []string{value}
+	return c
+}
+
 // SetCookieJar method sets custom http.CookieJar in the resty client. Its way to override default.
 //
 // For Example: sometimes we don't want to save cookies in api contacting, we can remove the default
@@ -591,6 +605,13 @@ func (c *Client) AddRetryAfterErrorCondition() *Client {
 	return c
 }
 
+// AddRetryHook adds a side-effecting retry hook to an array of hooks
+// that will be executed on each retry.
+func (c *Client) AddRetryHook(hook OnRetryFunc) *Client {
+	c.RetryHooks = append(c.RetryHooks, hook)
+	return c
+}
+
 // SetTLSClientConfig method sets TLSClientConfig for underling client Transport.
 //
 // For Example:
diff --git a/request.go b/request.go
index b1e8eb8..bf92cdd 100644
--- a/request.go
+++ b/request.go
@@ -117,6 +117,19 @@ func (r *Request) SetHeaders(headers map[string]string) *Request {
 	return r
 }
 
+// SetHeaderVerbatim method is to set a single header field and its value verbatim in the current request.
+//
+// For Example: To set `all_lowercase` and `UPPERCASE` as `available`.
+// 		client.R().
+//			SetHeaderVerbatim("all_lowercase", "available").
+//			SetHeaderVerbatim("UPPERCASE", "available")
+//
+// Also you can override header value, which was set at client instance level.
+func (r *Request) SetHeaderVerbatim(header, value string) *Request {
+	r.Header[header] = []string{value}
+	return r
+}
+
 // SetQueryParam method sets single parameter and its value in the current request.
 // It will be formed as query string for the request.
 //
@@ -733,6 +746,7 @@ func (r *Request) Execute(method, url string) (*Response, error) {
 		WaitTime(r.client.RetryWaitTime),
 		MaxWaitTime(r.client.RetryMaxWaitTime),
 		RetryConditions(r.client.RetryConditions),
+		RetryHooks(r.client.RetryHooks),
 	)
 
 	r.client.onErrorHooks(r, resp, unwrapNoRetryErr(err))
diff --git a/retry.go b/retry.go
index a56f3b4..d281236 100644
--- a/retry.go
+++ b/retry.go
@@ -26,6 +26,9 @@ type (
 	// input: non-nil Response OR request execution error
 	RetryConditionFunc func(*Response, error) bool
 
+	// OnRetryFunc is for side-effecting functions triggered on retry
+	OnRetryFunc func(*Response, error)
+
 	// RetryAfterFunc returns time to wait before retry
 	// For example, it can parse HTTP Retry-After header
 	// https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
@@ -39,6 +42,7 @@ type (
 		waitTime        time.Duration
 		maxWaitTime     time.Duration
 		retryConditions []RetryConditionFunc
+		retryHooks      []OnRetryFunc
 	}
 )
 
@@ -70,6 +74,13 @@ func RetryConditions(conditions []RetryConditionFunc) Option {
 	}
 }
 
+// RetryHooks sets the hooks that will be executed after each retry
+func RetryHooks(hooks []OnRetryFunc) Option {
+	return func(o *Options) {
+		o.retryHooks = hooks
+	}
+}
+
 // Backoff retries with increasing timeout duration up until X amount of retries
 // (Default is 3 attempts, Override with option Retries(n))
 func Backoff(operation func() (*Response, error), options ...Option) error {
@@ -114,6 +125,10 @@ func Backoff(operation func() (*Response, error), options ...Option) error {
 			return err
 		}
 
+		for _, hook := range opts.retryHooks {
+			hook(resp, err)
+		}
+
 		waitTime, err2 := sleepDuration(resp, opts.waitTime, opts.maxWaitTime, attempt)
 		if err2 != nil {
 			if err == nil {


Test patch:
diff --git a/client_test.go b/client_test.go
index 166b050..03e0c0e 100644
--- a/client_test.go
+++ b/client_test.go
@@ -253,6 +253,18 @@ func TestClientOnBeforeRequestModification(t *testing.T) {
 	logResponse(t, resp)
 }
 
+func TestClientSetHeaderVerbatim(t *testing.T) {
+	ts := createPostServer(t)
+	defer ts.Close()
+
+	c := dc().
+		SetHeaderVerbatim("header-lowercase", "value_lowercase").
+		SetHeader("header-lowercase", "value_standard")
+
+	assertEqual(t, "value_lowercase", strings.Join(c.Header["header-lowercase"], ""))
+	assertEqual(t, "value_standard", strings.Join(c.Header["Header-Lowercase"], ""))
+}
+
 func TestClientSetTransport(t *testing.T) {
 	ts := createGetServer(t)
 	defer ts.Close()
diff --git a/request_test.go b/request_test.go
index 100fb32..cf7cca6 100644
--- a/request_test.go
+++ b/request_test.go
@@ -1359,6 +1359,19 @@ func TestSetQueryStringTypical(t *testing.T) {
 	assertEqual(t, "TestGet: text response", resp.String())
 }
 
+func TestSetHeaderVerbatim(t *testing.T) {
+	ts := createPostServer(t)
+	defer ts.Close()
+
+	r := dclr().
+		SetHeaderVerbatim("header-lowercase", "value_lowercase").
+		SetHeader("header-lowercase", "value_standard")
+
+	assertEqual(t, "value_lowercase", strings.Join(r.Header["header-lowercase"], ""))
+	assertEqual(t, "value_standard", strings.Join(r.Header["Header-Lowercase"], ""))
+}
+
+
 func TestOutputFileWithBaseDirAndRelativePath(t *testing.T) {
 	ts := createGetServer(t)
 	defer ts.Close()
diff --git a/retry_test.go b/retry_test.go
index 19d274d..596791c 100644
--- a/retry_test.go
+++ b/retry_test.go
@@ -77,6 +77,23 @@ func TestConditionalBackoffConditionNonExecution(t *testing.T) {
 	assertNotEqual(t, counter, attempts)
 }
 
+// Check to make sure that RetryHooks are executed
+func TestOnRetryBackoff(t *testing.T) {
+	attempts := 3
+	counter := 0
+
+	hook := func(r *Response, err error) {
+		counter++
+	}
+
+	retryErr := Backoff(func() (*Response, error) {
+		return nil, nil
+	}, RetryHooks([]OnRetryFunc{hook}))
+
+	assertError(t, retryErr)
+	assertNotEqual(t, counter, attempts)
+}
+
 // Check to make sure the functions added to add conditionals work
 func TestConditionalGet(t *testing.T) {
 	ts := createGetServer(t)
@@ -651,6 +668,35 @@ func TestClientErrorRetry(t *testing.T) {
 	assertNil(t, resp.Error())
 }
 
+func TestClientRetryHook(t *testing.T) {
+	ts := createGetServer(t)
+	defer ts.Close()
+
+	attempt := 0
+
+	c := dc().
+		SetRetryCount(2).
+		SetTimeout(time.Second * 3).
+		AddRetryHook(
+			func(r *Response, _ error) {
+				attempt++
+			},
+		)
+
+	resp, err := c.R().Get(ts.URL + "/set-retrycount-test")
+	assertEqual(t, "", resp.Status())
+	assertEqual(t, "", resp.Proto())
+	assertEqual(t, 0, resp.StatusCode())
+	assertEqual(t, 0, len(resp.Cookies()))
+	assertNotNil(t, resp.Body())
+	assertEqual(t, 0, len(resp.Header()))
+
+	assertEqual(t, 3, attempt)
+
+	assertEqual(t, true, (strings.HasPrefix(err.Error(), "Get "+ts.URL+"/set-retrycount-test") ||
+		strings.HasPrefix(err.Error(), "Get \""+ts.URL+"/set-retrycount-test\"")))
+}
+
 func filler(*Response, error) bool {
 	return false
 }