Skip to content

Instantly share code, notes, and snippets.

@hackerzhut
Created November 28, 2018 03:26
Show Gist options
  • Save hackerzhut/0a552f419e81266a34cc2c9b5595f1f2 to your computer and use it in GitHub Desktop.
Save hackerzhut/0a552f419e81266a34cc2c9b5595f1f2 to your computer and use it in GitHub Desktop.
Sample HTTP Go Client
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