-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 35371eb
Showing
12 changed files
with
558 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
MIT |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
#Makefile | ||
|
||
# Test | ||
test: | ||
go test -v -cover -coverprofile=coverage.out ./... | ||
|
||
.phony: test |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
## Clink: A Configurable HTTP Client for Go | ||
|
||
Clink is a highly configurable HTTP client for Go, designed for ease of use, extendability, and robustness. It supports various features like automatic retries, request rate limiting, and customisable middlewares, making it ideal for both simple and advanced HTTP requests. | ||
|
||
### Features | ||
- **Flexible Request Options**: Easily configure headers, URLs, and authentication. | ||
- **Retry Mechanism**: Automatic retries with configurable policies. | ||
- **Middleware Support**: Extend functionality with custom middleware modules. | ||
- **Rate Limiting**: Client-side rate limiting to avoid server-side limits. | ||
- **Logging & Tracing**: Built-in support for logging and distributed tracing. | ||
|
||
### Installation | ||
To use Clink in your Go project, install it using `go get`: | ||
|
||
```bash | ||
go get -u github.com/davesavic/clink | ||
``` | ||
|
||
### Usage | ||
Here is a basic example of how to use Clink: | ||
|
||
```go | ||
TODO | ||
``` | ||
|
||
### Contributing | ||
Contributions to Clink are welcome! If you find a bug, have a feature request, or want to contribute code, please open an issue or submit a pull request. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,158 @@ | ||
package clink | ||
|
||
import ( | ||
"encoding/base64" | ||
"encoding/json" | ||
"fmt" | ||
"golang.org/x/time/rate" | ||
"io" | ||
"net/http" | ||
"time" | ||
) | ||
|
||
type Client struct { | ||
HttpClient *http.Client | ||
Headers map[string]string | ||
RateLimiter *rate.Limiter | ||
MaxRetries int | ||
ShouldRetryFunc func(*http.Request, *http.Response, error) bool | ||
} | ||
|
||
func NewClient(opts ...Option) *Client { | ||
c := defaultClient() | ||
|
||
for _, opt := range opts { | ||
opt(c) | ||
} | ||
|
||
return c | ||
} | ||
|
||
func defaultClient() *Client { | ||
return &Client{ | ||
HttpClient: http.DefaultClient, | ||
Headers: make(map[string]string), | ||
} | ||
} | ||
|
||
func (c *Client) Do(req *http.Request) (*http.Response, error) { | ||
for key, value := range c.Headers { | ||
req.Header.Set(key, value) | ||
} | ||
|
||
if c.RateLimiter != nil { | ||
if err := c.RateLimiter.Wait(req.Context()); err != nil { | ||
return nil, fmt.Errorf("failed to wait for rate limiter: %w", err) | ||
} | ||
} | ||
|
||
var resp *http.Response | ||
var err error | ||
|
||
for attempt := 0; attempt <= c.MaxRetries; attempt++ { | ||
resp, err = c.HttpClient.Do(req) | ||
if err == nil { | ||
break | ||
} | ||
|
||
if c.ShouldRetryFunc != nil && !c.ShouldRetryFunc(req, resp, err) { | ||
break | ||
} | ||
|
||
if attempt < c.MaxRetries { | ||
// Exponential backoff only if we're going to retry. | ||
time.Sleep(time.Duration(attempt) * time.Second) | ||
} | ||
} | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("failed to do request: %w", err) | ||
} | ||
|
||
return resp, nil | ||
} | ||
|
||
type Option func(*Client) | ||
|
||
// WithClient sets the http client for the client. | ||
func WithClient(client *http.Client) Option { | ||
return func(c *Client) { | ||
c.HttpClient = client | ||
} | ||
} | ||
|
||
// WithHeader sets a header for the client. | ||
func WithHeader(key, value string) Option { | ||
return func(c *Client) { | ||
c.Headers[key] = value | ||
} | ||
} | ||
|
||
// WithHeaders sets the headers for the client. | ||
func WithHeaders(headers map[string]string) Option { | ||
return func(c *Client) { | ||
c.Headers = headers | ||
} | ||
} | ||
|
||
// WithRateLimit sets the rate limit for the client in requests per minute. | ||
func WithRateLimit(rpm int) Option { | ||
return func(c *Client) { | ||
interval := time.Minute / time.Duration(rpm) | ||
c.RateLimiter = rate.NewLimiter(rate.Every(interval), 1) | ||
} | ||
} | ||
|
||
// WithBasicAuth sets the basic auth header for the client. | ||
func WithBasicAuth(username, password string) Option { | ||
return func(c *Client) { | ||
c.Headers["Authorization"] = "Basic " + basicAuth(username, password) | ||
} | ||
} | ||
|
||
func basicAuth(username, password string) string { | ||
auth := username + ":" + password | ||
return base64.StdEncoding.EncodeToString([]byte(auth)) | ||
} | ||
|
||
// WithBearerAuth sets the bearer auth header for the client. | ||
func WithBearerAuth(token string) Option { | ||
return func(c *Client) { | ||
c.Headers["Authorization"] = "Bearer " + token | ||
} | ||
} | ||
|
||
// WithUserAgent sets the user agent header for the client. | ||
func WithUserAgent(ua string) Option { | ||
return func(c *Client) { | ||
c.Headers["User-Agent"] = ua | ||
} | ||
} | ||
|
||
// WithRetries sets the retry count and retry function for the client. | ||
func WithRetries(count int, retryFunc func(*http.Request, *http.Response, error) bool) Option { | ||
return func(c *Client) { | ||
c.MaxRetries = count | ||
c.ShouldRetryFunc = retryFunc | ||
} | ||
} | ||
|
||
func ResponseToJson[T any](response *http.Response, target *T) error { | ||
if response == nil { | ||
return fmt.Errorf("response is nil") | ||
} | ||
|
||
if response.Body == nil { | ||
return fmt.Errorf("response body is nil") | ||
} | ||
|
||
defer func(Body io.ReadCloser) { | ||
_ = Body.Close() | ||
}(response.Body) | ||
|
||
if err := json.NewDecoder(response.Body).Decode(target); err != nil { | ||
return fmt.Errorf("failed to decode response: %w", err) | ||
} | ||
|
||
return nil | ||
} |
Oops, something went wrong.