Skip to content

Commit 0b9ae7f

Browse files
committed
Add Retry and Cache middleware implementations
Implement Retry middleware to support retry mechanisms for HTTP requests, offering various backoff strategies and configurations. Implement Cache middleware for in-memory caching of HTTP responses, allowing custom cache key strategies and TTL-based eviction. Also, add test coverage for both middlewares to ensure correct behavior under different conditions. Update README to document new middlewares with examples and configuration options.
1 parent d2c36e8 commit 0b9ae7f

File tree

5 files changed

+1313
-17
lines changed

5 files changed

+1313
-17
lines changed

README.md

Lines changed: 135 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,18 @@ _Please note: this is not a replacement for `http.Client`, but rather a companio
2626

2727
`go get -u github.com/go-pkgz/requester`
2828

29-
30-
## `Requester` middlewares
31-
3229
## Overview
3330

31+
*Built-in middlewares:*
32+
3433
- `Header` - appends user-defined headers to all requests.
34+
- `MaxConcurrent` - sets maximum concurrency
35+
- `Retry` - sets retry on errors and status codes
3536
- `JSON` - sets headers `"Content-Type": "application/json"` and `"Accept": "application/json"`
3637
- `BasicAuth(user, passwd string)` - adds HTTP Basic Authentication
37-
- `MaxConcurrent` - sets maximum concurrency
38+
39+
*Interfaces for external middlewares:*
40+
3841
- `Repeater` - sets repeater to retry failed requests. Doesn't provide repeater implementation but wraps it. Compatible with any repeater (for example [go-pkgz/repeater](https://github.com/go-pkgz/repeater)) implementing a single method interface `Do(ctx context.Context, fun func() error, errors ...error) (err error)` interface.
3942
- `Cache` - sets any `LoadingCache` implementation to be used for request/response caching. Doesn't provide cache, but wraps it. Compatible with any cache (for example a family of caches from [go-pkgz/lcw](https://github.com/go-pkgz/lcw)) implementing a single-method interface `Get(key string, fn func() (interface{}, error)) (val interface{}, err error)`
4043
- `Logger` - sets logger, compatible with any implementation of a single-method interface `Logf(format string, args ...interface{})`, for example [go-pkgz/lgr](https://github.com/go-pkgz/lgr)
@@ -45,7 +48,131 @@ Convenient functional adapter `middleware.RoundTripperFunc` provided.
4548

4649
See examples of the usage in [_example](https://github.com/go-pkgz/requester/tree/master/_example)
4750

48-
### Logging
51+
### Header middleware
52+
53+
`Header` middleware adds user-defined headers to all requests. It expects a map of headers to be added. For example:
54+
55+
```go
56+
rq := requester.New(http.Client{}, middleware.Header("X-Auth", "123456789"))
57+
```
58+
### MaxConcurrent middleware
59+
60+
`MaxConcurrent` middleware can be used to limit the concurrency of a given requester and limit overall concurrency for multiple requesters. For the first case, `MaxConcurrent(N)` should be created in the requester chain of middlewares. For example, `rq := requester.New(http.Client{Timeout: 3 * time.Second}, middleware.MaxConcurrent(8))`. To make it global, `MaxConcurrent` should be created once, outside the chain, and passed into each requester. For example:
61+
62+
```go
63+
mc := middleware.MaxConcurrent(16)
64+
rq1 := requester.New(http.Client{Timeout: 3 * time.Second}, mc)
65+
rq2 := requester.New(http.Client{Timeout: 1 * time.Second}, middleware.JSON, mc)
66+
```
67+
### Retry middleware
68+
69+
Retry middleware provides a flexible retry mechanism with different backoff strategies. By default, it retries on network errors and 5xx responses.
70+
71+
```go
72+
// retry 3 times with exponential backoff, starting from 100ms
73+
rq := requester.New(http.Client{}, middleware.Retry(3, 100*time.Millisecond))
74+
75+
// retry with custom options
76+
rq := requester.New(http.Client{}, middleware.Retry(3, 100*time.Millisecond,
77+
middleware.RetryWithBackoff(middleware.BackoffLinear), // use linear backoff
78+
middleware.RetryMaxDelay(5*time.Second), // cap maximum delay
79+
middleware.RetryWithJitter(0.1), // add 10% randomization
80+
middleware.RetryOnCodes(503, 502), // retry only on specific codes
81+
// or middleware.RetryExcludeCodes(404, 401), // alternatively, retry on all except these codes
82+
))
83+
```
84+
85+
Default configuration:
86+
- 3 attempts
87+
- Initial delay: 100ms
88+
- Max delay: 30s
89+
- Exponential backoff
90+
- 10% jitter
91+
- Retries on 5xx status codes
92+
93+
Retry Options:
94+
- `RetryWithBackoff(t BackoffType)` - set backoff strategy (Constant, Linear, or Exponential)
95+
- `RetryMaxDelay(d time.Duration)` - cap the maximum delay between retries
96+
- `RetryWithJitter(f float64)` - add randomization to delays (0-1.0 factor)
97+
- `RetryOnCodes(codes ...int)` - retry only on specific status codes
98+
- `RetryExcludeCodes(codes ...int)` - retry on all codes except specified
99+
100+
Note: `RetryOnCodes` and `RetryExcludeCodes` are mutually exclusive and can't be used together.
101+
102+
### Cache middleware
103+
104+
Cache middleware provides an **in-memory caching layer** for HTTP responses. It improves performance by avoiding repeated network calls for the same request.
105+
106+
#### **Basic Usage**
107+
108+
```go
109+
rq := requester.New(http.Client{}, middleware.Cache())
110+
```
111+
112+
By default:
113+
114+
- Only GET requests are cached
115+
- TTL (Time-To-Live) is 5 minutes
116+
- Maximum cache size is 1000 entries
117+
- Caches only HTTP 200 responses
118+
119+
120+
#### **Cache Configuration Options**
121+
122+
```go
123+
rq := requester.New(http.Client{}, middleware.Cache(
124+
middleware.CacheTTL(10*time.Minute), // change TTL to 10 minutes
125+
middleware.CacheSize(500), // limit cache to 500 entries
126+
middleware.CacheMethods(http.MethodGet, http.MethodPost), // allow caching for GET and POST
127+
middleware.CacheStatuses(200, 201, 204), // cache only responses with these status codes
128+
middleware.CacheWithBody, // include request body in cache key
129+
middleware.CacheWithHeaders("Authorization", "X-Custom-Header"), // include selected headers in cache key
130+
))
131+
```
132+
133+
#### Cache Key Composition
134+
135+
By default, the cache key is generated using:
136+
137+
- HTTP **method**
138+
- Full **URL**
139+
- (Optional) **Headers** (if `CacheWithHeaders` is enabled)
140+
- (Optional) **Body** (if `CacheWithBody` is enabled)
141+
142+
For example, enabling `CacheWithHeaders("Authorization")` will cache the same URL differently **for each unique Authorization token**.
143+
144+
#### Cache Eviction Strategy
145+
146+
- **Entries expire** when the TTL is reached.
147+
- **If the cache reaches its maximum size**, the **oldest entry is evicted** (FIFO order).
148+
149+
150+
#### Cache Limitations
151+
152+
- **Only caches complete HTTP responses.** Streaming responses are **not** supported.
153+
- **Does not cache responses with status codes other than 200** (unless explicitly allowed).
154+
- **Uses in-memory storage**, meaning the cache **resets on application restart**.
155+
156+
157+
### JSON middleware
158+
159+
`JSON` middleware sets headers `"Content-Type": "application/json"` and `"Accept": "application/json"`.
160+
161+
```go
162+
rq := requester.New(http.Client{}, middleware.JSON)
163+
```
164+
165+
### BasicAuth middleware
166+
167+
`BasicAuth` middleware adds HTTP Basic Authentication to all requests. It expects a username and password. For example:
168+
169+
```go
170+
rq := requester.New(http.Client{}, middleware.BasicAuth("user", "passwd"))
171+
```
172+
173+
----
174+
175+
### Logging middleware interface
49176

50177
Logger should implement `Logger` interface with a single method `Logf(format string, args ...interface{})`.
51178
For convenience, func type `LoggerFunc` is provided as an adapter to allow the use of ordinary functions as `Logger`.
@@ -63,19 +190,11 @@ logging options:
63190

64191
Note: If logging is allowed, it will log the URL, method, and may log headers and the request body. It may affect application security. For example, if a request passes some sensitive information as part of the body or header. In this case, consider turning logging off or providing your own logger to suppress all that you need to hide.
65192

66-
### MaxConcurrent
67-
68-
MaxConcurrent middleware can be used to limit the concurrency of a given requester and limit overall concurrency for multiple requesters. For the first case, `MaxConcurrent(N)` should be created in the requester chain of middlewares. For example, `rq := requester.New(http.Client{Timeout: 3 * time.Second}, middleware.MaxConcurrent(8))`. To make it global, `MaxConcurrent` should be created once, outside the chain, and passed into each requester. For example:
69-
70-
```go
71-
mc := middleware.MaxConcurrent(16)
72-
rq1 := requester.New(http.Client{Timeout: 3 * time.Second}, mc)
73-
rq2 := requester.New(http.Client{Timeout: 1 * time.Second}, middleware.JSON, mc)
74-
```
75193

76194
If the request is limited, it will wait till the limit is released.
77195

78-
### Cache
196+
### Cache middleware interface
197+
79198
Cache expects the `LoadingCache` interface to implement a single method: `Get(key string, fn func() (interface{}, error)) (val interface{}, err error)`. [LCW](https://github.com/go-pkgz/lcw/) can be used directly, and in order to adopt other caches, see the provided `LoadingCacheFunc`.
80199

81200
#### Caching Key and Allowed Requests
@@ -92,12 +211,11 @@ Several options define what part of the request will be used for the key:
92211

93212
example: `cache.New(lruCache, cache.Methods("GET", "POST"), cache.KeyFunc() {func(r *http.Request) string {return r.Host})`
94213

95-
96214
#### cache and streaming response
97215

98216
`Cache` is **not compatible** with HTTP streaming mode. Practically, this is rare and exotic, but allowing `Cache` will effectively transform the streaming response into a "get it all" typical response. This is due to the fact that the cache has to read the response body fully to save it, so technically streaming will be working, but the client will receive all the data at once.
99217

100-
### Repeater
218+
### Repeater middleware interface
101219

102220
`Repeater` expects a single method interface `Do(fn func() error, failOnCodes ...error) (err error)`. [repeater](github.com/go-pkgz/repeater) can be used directly.
103221

0 commit comments

Comments
 (0)