Created
November 28, 2018 03:26
-
-
Save hackerzhut/0a552f419e81266a34cc2c9b5595f1f2 to your computer and use it in GitHub Desktop.
Sample HTTP Go Client
This file contains hidden or 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
package client | |
import ( | |
"bytes" | |
"context" | |
"encoding/json" | |
"encoding/xml" | |
"fmt" | |
"github.com/telematicsct/gatekeeper/pkg/log" | |
"go.uber.org/zap" | |
"io" | |
"io/ioutil" | |
"net" | |
"net/http" | |
"net/url" | |
"path" | |
"runtime" | |
"time" | |
) | |
const ( | |
//MediaTypeJSON json media type | |
MediaTypeJSON = "application/json" | |
//MediaTypeXML xml media type | |
MediaTypeXML = "application/xml" | |
) | |
/* | |
https://github.com/tamnd/httpclient/blob/master/client.go | |
https://github.com/starboy/httpclient/blob/master/client.go [Check Do] | |
https://github.com/gojektech/heimdall/blob/master/httpclient/client.go [GET, POST] | |
*/ | |
//Client HTTP Client Wrapper | |
type Client struct { | |
// token is the authorization token | |
authToken string | |
// client is the http.Client singleton used for wire interaction | |
hc *http.Client | |
// baseURL is the base endpoint of the remote service | |
baseURL *url.URL | |
logger log.Factory | |
} | |
// DefaultTransport returns a new http.Transport with similar default values to | |
// http.DefaultTransport, but with idle connections and keepalives disabled. | |
func DefaultTransport() *http.Transport { | |
transport := DefaultPooledTransport() | |
transport.DisableKeepAlives = true | |
transport.MaxIdleConnsPerHost = -1 | |
return transport | |
} | |
// DefaultPooledTransport returns a new http.Transport with similar default | |
// values to http.DefaultTransport. Do not use this for transient transports as | |
// it can leak file descriptors over time. Only use this for transports that | |
// will be re-used for the same host(s). | |
func DefaultPooledTransport() *http.Transport { | |
transport := &http.Transport{ | |
Proxy: http.ProxyFromEnvironment, | |
DialContext: (&net.Dialer{ | |
Timeout: 30 * time.Second, | |
KeepAlive: 30 * time.Second, | |
DualStack: true, | |
}).DialContext, | |
MaxIdleConns: 100, | |
IdleConnTimeout: 90 * time.Second, | |
TLSHandshakeTimeout: 10 * time.Second, | |
ExpectContinueTimeout: 1 * time.Second, | |
MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1, | |
} | |
return transport | |
} | |
// New creates and returns a new HTTP Client with defaults. | |
// httpClient can be nil, in which case http.DefaultClient will be used. | |
func New(bURL string, authToken string, logger log.Factory) *Client { | |
baseURL, _ := url.Parse(bURL) | |
c := &Client{ | |
authToken: authToken, | |
hc: &http.Client{ | |
Transport: &authTransport{ | |
authToken: authToken, | |
roundTripper: DefaultTransport(), | |
// roundTripper: &http.Transport{ | |
// Dial: (&net.Dialer{KeepAlive: 600 * time.Second}).Dial, | |
// MaxIdleConns: 100, | |
// MaxIdleConnsPerHost: 100, | |
// }, | |
logger: logger, | |
}, | |
Timeout: 30 * time.Second, | |
}, | |
baseURL: baseURL, | |
logger: logger, | |
} | |
return c | |
} | |
// NewRequest creates an API request. A relative URL can be provided in urlStr, in which case it is resolved relative to the BaseURL | |
// of the Client. Relative URLs should always be specified without a preceding slash. If specified, the value pointed to | |
// by body is included as the request body. | |
func (c *Client) NewRequest(method, uri, contentType string, body *bytes.Buffer) (*http.Request, error) { | |
u, err := c.baseURL.Parse(path.Join(c.baseURL.Path, uri)) | |
if err != nil { | |
return nil, fmt.Errorf("error parsing url %s", uri) | |
} | |
req, err := http.NewRequest(method, u.String(), body) | |
if err != nil { | |
return nil, fmt.Errorf("error constructing http request %s", uri) | |
} | |
req.Header.Add("Content-Type", contentType) | |
return req, nil | |
} | |
// Do executes a give request with the given context. If the parameter v is a writer the body will be written to it in | |
// raw format, else v is assumed to be a struct to unmarshal the body into assuming XML format. If v is nil then the | |
// body is not read and can be manually parsed from the response | |
func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*http.Response, error) { | |
// req.Header.Add("Content-Type", "application/xml; charset=utf-8") | |
req = req.WithContext(ctx) | |
res, err := c.hc.Do(req) | |
if err != nil { | |
select { | |
//If context has been cancelled then context's error need to be sent back | |
case <-ctx.Done(): | |
return nil, ctx.Err() | |
default: | |
} | |
if e, ok := err.(*url.Error); ok { | |
if url2, err := url.Parse(e.URL); err == nil { | |
e.URL = url2.String() | |
return nil, e | |
} | |
} | |
return nil, err | |
} | |
defer res.Body.Close() | |
if v != nil { | |
if w, ok := v.(io.Writer); ok { | |
io.Copy(w, res.Body) | |
} else { | |
switch res.Header["Content-Type"][0] { | |
case MediaTypeJSON: | |
err = json.NewDecoder(res.Body).Decode(v) | |
default: | |
err = xml.NewDecoder(res.Body).Decode(v) | |
} | |
if err == io.EOF { | |
err = nil // ignore EOF errors caused by empty response body | |
} | |
} | |
} | |
return res, err | |
} | |
// transport is a wrapper providing authentication, tracing and error handling. | |
type authTransport struct { | |
authToken string | |
roundTripper http.RoundTripper | |
logger log.Factory | |
} | |
var _ http.RoundTripper = &authTransport{} | |
func (t *authTransport) RoundTrip(req *http.Request) (res *http.Response, err error) { | |
startTime := time.Now() | |
// Incoming request's headers should not be modified as per http.RoundTripper specification. | |
// Make a deep copy before doing so. | |
req2 := new(http.Request) | |
deepCopyRequest(req, req2) | |
if req2.Header.Get("Authorization") == "" { | |
req2.Header.Set("Authorization", "Bearer 1312312") | |
// req2.Header.Set("Authorization", t.authToken) | |
} | |
if t.roundTripper == nil { | |
res, err = http.DefaultTransport.RoundTrip(req2) | |
} else { | |
res, err = t.roundTripper.RoundTrip(req2) | |
} | |
if err != nil { | |
t.logger.Bg().Info( | |
"", | |
zap.String("method", req.Method), | |
zap.String("host", req.Host), | |
zap.String("path", req.URL.Path), | |
zap.String("error", err.Error()), | |
zap.Stringer("took", time.Since(startTime)), | |
) | |
return nil, err | |
} | |
t.logger.Bg().Info( | |
"", | |
zap.String("method", req.Method), | |
zap.String("host", req.Host), | |
zap.String("path", req.URL.Path), | |
zap.Int("status", res.StatusCode), | |
zap.Stringer("took", time.Since(startTime)), | |
) | |
return res, err | |
} | |
// Checks the if the authorization token is valid. Will be refreshed if token is rotated. | |
// For now default token is returned | |
func (t *authTransport) Token() string { | |
// t.mu.Lock() | |
// defer t.mu.Unlock() | |
return t.authToken | |
} | |
func deepCopyRequest(req *http.Request, req2 *http.Request) { | |
*req2 = *req | |
req2.Header = make(http.Header, len(req.Header)) | |
for k, s := range req.Header { | |
req2.Header[k] = append([]string(nil), s...) | |
} | |
} | |
func parseError(res *http.Response) error { | |
defer drainAndClose(res.Body) | |
httpError := &Error{} | |
if err := json.NewDecoder(res.Body).Decode(httpError); err != nil { | |
return fmt.Errorf("%v %v: %d %+v", res.Request.Method, res.Request.URL, res.StatusCode, err) | |
} | |
return httpError | |
} | |
// drainAndClose will make an attempt at flushing and closing the body so that the | |
// underlying connection can be reused. It will not read more than 10KB. | |
func drainAndClose(body io.ReadCloser) { | |
io.CopyN(ioutil.Discard, body, 10*1024) | |
body.Close() | |
} | |
// Error is the decoded B2 JSON error return value. It's not the only type of | |
// error returned by this package, and it is mostly returned wrapped in a | |
// url.Error. Use UnwrapError to access it. | |
type Error struct { | |
Code string | |
Message string | |
Status int | |
} | |
func (e *Error) Error() string { | |
return fmt.Sprintf("httperror [%s]: %s", e.Code, e.Message) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment