-
Notifications
You must be signed in to change notification settings - Fork 1
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
Showing
7 changed files
with
649 additions
and
0 deletions.
There are no files selected for viewing
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,19 @@ | ||
.PHONY: deps test cover sync-coveralls mocks | ||
|
||
deps: | ||
go mod download | ||
|
||
test: deps | ||
go test ./... | ||
|
||
covertest: deps | ||
go test -coverprofile=coverage.out ./... | ||
go tool cover -html=coverage.out | ||
|
||
sync-coveralls: deps | ||
go test -coverprofile=coverage.out ./... | ||
goveralls -coverprofile=coverage.out -reponame=httpclient -repotoken=${COVERALLS_HTTPCLIENT_TOKEN} -service=local | ||
|
||
mocks: deps | ||
mockgen -package=httpclient -destination=client_mock.go . Doer | ||
|
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,59 @@ | ||
package httpclient | ||
|
||
import ( | ||
"context" | ||
"io" | ||
"net/http" | ||
"time" | ||
) | ||
|
||
// 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(ctx context.Context, url string, headers http.Header) (*http.Response, error) | ||
Post(ctx context.Context, url string, body io.Reader, headers http.Header) (*http.Response, error) | ||
Put(ctx context.Context, url string, body io.Reader, headers http.Header) (*http.Response, error) | ||
Delete(ctx context.Context, url string, headers http.Header) (*http.Response, error) | ||
Do(req *http.Request) (*http.Response, error) | ||
} | ||
|
||
// RequestHook allows a function to run before each retry. The HTTP | ||
// request which will be made, and the retry number (0 for the initial | ||
// request) are available to users. | ||
type RequestHook func(*http.Request, int) | ||
|
||
// ResponseHook is like RequestHook, but allows running a function | ||
// on each HTTP response. This function will be invoked at the end of | ||
// every HTTP request executed, regardless of whether a subsequent retry | ||
// needs to be performed or not. If the response body is read or closed | ||
// from this method, this will affect the response returned from Do(). | ||
type ResponseHook func(*http.Request, *http.Response) | ||
|
||
// CheckRetry specifies a policy for handling retries. It is called | ||
// following each request with the response and error values returned by | ||
// the http.Client. If CheckRetry returns false, the Client stops retrying | ||
// and returns the response to the caller. If CheckRetry returns an error, | ||
// that error value is returned in lieu of the error from the request. The | ||
// Client will close any response body when retrying, but if the retry is | ||
// aborted it is up to the CheckRetry callback to properly close any | ||
// response body before returning. | ||
type CheckRetry func(req *http.Request, resp *http.Response, err error) (bool, error) | ||
|
||
// BackOff specifies a policy for how long to wait between retries. | ||
// It is called after a failing request to determine the amount of time | ||
// that should pass before trying again. | ||
type BackOff func(attemptNum int, resp *http.Response) time.Duration | ||
|
||
// ErrorHandler is called if retries are expired, containing the last status | ||
// from the http library. If not specified, default behavior for the library is | ||
// to close the body and return an error indicating how many tries were | ||
// attempted. If overriding this, be sure to close the body if needed. | ||
type ErrorHandler func(resp *http.Response, err error, numTries int) (*http.Response, error) | ||
|
||
// ErrorHook is called when the request returned a connection error. | ||
type ErrorHook func(req *http.Request, err error, retry int) |
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,10 @@ | ||
module github.com/mediabuyerbot/httpclient | ||
|
||
go 1.14 | ||
|
||
require ( | ||
github.com/gojek/valkyrie v0.0.0-20190210220504-8f62c1e7ba45 | ||
github.com/golang/mock v1.4.1 | ||
github.com/pkg/errors v0.9.1 | ||
github.com/stretchr/testify v1.5.1 | ||
) |
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,200 @@ | ||
package httpclient | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"io" | ||
"io/ioutil" | ||
"net/http" | ||
"time" | ||
|
||
"github.com/gojek/valkyrie" | ||
|
||
"github.com/pkg/errors" | ||
) | ||
|
||
const ( | ||
DefaultHTTPTimeout = 60 * time.Second | ||
) | ||
|
||
// HttpClient is the http client implementation | ||
type HttpClient struct { | ||
baseURL string | ||
client Doer | ||
retryCount int | ||
requestHook RequestHook | ||
responseHook ResponseHook | ||
errorHook ErrorHook | ||
checkRetry CheckRetry | ||
backOff BackOff | ||
errorHandler ErrorHandler | ||
} | ||
|
||
var defaultBackOffPolicy = func(attemptNum int, resp *http.Response) time.Duration { | ||
return 500 * time.Millisecond | ||
} | ||
|
||
// New returns a new instance of Client. | ||
func New(opts ...Option) (Client, error) { | ||
client := HttpClient{ | ||
backOff: defaultBackOffPolicy, | ||
client: &http.Client{ | ||
Timeout: DefaultHTTPTimeout, | ||
}, | ||
} | ||
for _, opt := range opts { | ||
opt(&client) | ||
} | ||
return &client, nil | ||
} | ||
|
||
// Get makes a HTTP GET request to provided URL. | ||
func (c *HttpClient) Get(ctx context.Context, url string, headers http.Header) (*http.Response, error) { | ||
var response *http.Response | ||
if len(c.baseURL) > 0 { | ||
url = c.baseURL + url | ||
} | ||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) | ||
if err != nil { | ||
return response, errors.Wrap(err, "GET - request creation failed") | ||
} | ||
request.Header = headers | ||
return c.Do(request) | ||
} | ||
|
||
// Post makes a HTTP POST request to provided URL and requestBody. | ||
func (c *HttpClient) Post(ctx context.Context, url string, body io.Reader, headers http.Header) (*http.Response, error) { | ||
var response *http.Response | ||
if len(c.baseURL) > 0 { | ||
url = c.baseURL + url | ||
} | ||
request, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body) | ||
if err != nil { | ||
return response, errors.Wrap(err, "POST - request creation failed") | ||
} | ||
|
||
request.Header = headers | ||
|
||
return c.Do(request) | ||
} | ||
|
||
// Put makes a HTTP PUT request to provided URL and requestBody. | ||
func (c *HttpClient) Put(ctx context.Context, url string, body io.Reader, headers http.Header) (*http.Response, error) { | ||
var response *http.Response | ||
if len(c.baseURL) > 0 { | ||
url = c.baseURL + url | ||
} | ||
request, err := http.NewRequestWithContext(ctx, http.MethodPut, url, body) | ||
if err != nil { | ||
return response, errors.Wrap(err, "PUT - request creation failed") | ||
} | ||
|
||
request.Header = headers | ||
|
||
return c.Do(request) | ||
} | ||
|
||
// Delete makes a HTTP DELETE request with provided URL. | ||
func (c *HttpClient) Delete(ctx context.Context, url string, headers http.Header) (*http.Response, error) { | ||
var response *http.Response | ||
if len(c.baseURL) > 0 { | ||
url = c.baseURL + url | ||
} | ||
request, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil) | ||
if err != nil { | ||
return response, errors.Wrap(err, "DELETE - request creation failed") | ||
} | ||
|
||
request.Header = headers | ||
|
||
return c.Do(request) | ||
} | ||
|
||
// Do makes an HTTP request with the native `http.Do` interface. | ||
func (c *HttpClient) Do(req *http.Request) (resp *http.Response, err error) { | ||
var bodyReader *bytes.Reader | ||
|
||
req.Close = true | ||
if req.Body != nil { | ||
reqData, err := ioutil.ReadAll(req.Body) | ||
if err != nil { | ||
return nil, err | ||
} | ||
bodyReader = bytes.NewReader(reqData) | ||
req.Body = ioutil.NopCloser(bodyReader) | ||
} | ||
|
||
multiErr := &valkyrie.MultiError{} | ||
var numTries int | ||
for i := 0; i <= c.retryCount; i++ { | ||
isRetryOk := c.retryCount > 0 && i < c.retryCount | ||
if resp != nil && resp.Body != nil { | ||
_ = resp.Body.Close() | ||
} | ||
|
||
if c.requestHook != nil { | ||
c.requestHook(req, i) | ||
} | ||
|
||
var err error | ||
resp, err = c.client.Do(req) | ||
if bodyReader != nil { | ||
_, _ = bodyReader.Seek(0, 0) | ||
} | ||
if err != nil { | ||
if c.errorHook != nil { | ||
c.errorHook(req, err, i) | ||
} | ||
|
||
multiErr.Push(err.Error()) | ||
|
||
if c.checkRetry != nil { | ||
checkOK, checkErr := c.checkRetry(req, resp, err) | ||
if !checkOK { | ||
if checkErr != nil { | ||
multiErr.Push(checkErr.Error()) | ||
} | ||
break | ||
} | ||
} | ||
if isRetryOk { | ||
wait := c.backOff(i, resp) | ||
time.Sleep(wait) | ||
} | ||
numTries++ | ||
continue | ||
} | ||
|
||
if c.responseHook != nil { | ||
c.responseHook(req, resp) | ||
} | ||
|
||
var nextLoop bool | ||
isDefaultRetryPolicy := resp.StatusCode >= http.StatusInternalServerError && isRetryOk | ||
|
||
if c.checkRetry != nil && isRetryOk { | ||
checkOK, checkErr := c.checkRetry(req, resp, nil) | ||
if !checkOK { | ||
if checkErr != nil { | ||
multiErr.Push(checkErr.Error()) | ||
} | ||
break | ||
} | ||
nextLoop = true | ||
} else if isDefaultRetryPolicy { | ||
nextLoop = true | ||
} | ||
|
||
if nextLoop { | ||
wait := c.backOff(i, resp) | ||
time.Sleep(wait) | ||
numTries++ | ||
continue | ||
} | ||
break | ||
} | ||
if c.errorHandler != nil { | ||
return c.errorHandler(resp, multiErr.HasError(), numTries) | ||
} | ||
return resp, multiErr.HasError() | ||
} |
Oops, something went wrong.