bitbucket.org/ai69/amoy@v0.2.3/http.go (about)

     1  package amoy
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/base64"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"net/http/httputil"
    12  	"net/url"
    13  	"strings"
    14  	"time"
    15  
    16  	"github.com/1set/gut/ystring"
    17  )
    18  
    19  // HTTPClientOptions represents options for HTTPClient.
    20  type HTTPClientOptions struct {
    21  	// Client is the underlying http.Client, you can set it to use your own client with transport.
    22  	Client *http.Client
    23  	// Timeout is the maximum amount of time a dial will wait for a connect to complete.
    24  	Timeout time.Duration
    25  	// UserAgent is the User-Agent header value.
    26  	UserAgent string
    27  	// Headers is the default HTTP headers.
    28  	Headers map[string]string
    29  	// Insecure indicates whether to skip TLS verification.
    30  	Insecure bool
    31  	// DisableRedirect indicates whether to disable redirect for HTTP 301, 302, 303, 307, 308.
    32  	DisableRedirect bool
    33  	// Username is the username for basic authentication.
    34  	Username string
    35  	// Password is the password for basic authentication.
    36  	Password string
    37  	// BearerToken is the bearer token for token authentication. It will override the basic authentication if it's set together.
    38  	BearerToken string
    39  }
    40  
    41  // HTTPClient is a wrapper of http.Client with some helper methods.
    42  type HTTPClient struct {
    43  	*http.Client
    44  	timeout time.Duration
    45  	headers map[string]string // default headers for user agent, auth, etc.
    46  }
    47  
    48  var (
    49  	defaultHTTPClientOpts = &HTTPClientOptions{
    50  		Timeout:   30 * time.Second,
    51  		UserAgent: fmt.Sprintf("Go-amoy-http/%s", Version), // "User-Agent": "Go-http-client/2.0",
    52  	}
    53  )
    54  
    55  // NewHTTPClient creates a new HTTPClient.
    56  func NewHTTPClient(opts *HTTPClientOptions) *HTTPClient {
    57  	// if opts is nil, use default options
    58  	if opts == nil {
    59  		opts = defaultHTTPClientOpts
    60  	} else {
    61  		// if opts is not nil, but some fields are empty, use default options
    62  		if opts.Timeout <= 0 {
    63  			opts.Timeout = defaultHTTPClientOpts.Timeout
    64  		}
    65  		if ystring.IsEmpty(opts.UserAgent) {
    66  			opts.UserAgent = defaultHTTPClientOpts.UserAgent
    67  		}
    68  	}
    69  
    70  	// create a new HTTPClient
    71  	hc := opts.Client
    72  	if hc == nil {
    73  		// didn't bring its own HTTP client, create a new one with timeout
    74  		hc = &http.Client{
    75  			Timeout: opts.Timeout,
    76  		}
    77  	}
    78  	cli := &HTTPClient{
    79  		Client:  hc,
    80  		timeout: opts.Timeout,
    81  		headers: make(map[string]string, 2+len(opts.Headers)),
    82  	}
    83  
    84  	// clone the default transport and set InsecureSkipVerify to true
    85  	if opts.Insecure {
    86  		tr := http.DefaultTransport.(*http.Transport).Clone()
    87  		tr.TLSClientConfig.InsecureSkipVerify = true
    88  		cli.Client.Transport = tr
    89  	}
    90  
    91  	// disable redirect
    92  	if opts.DisableRedirect {
    93  		cli.Client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
    94  			return http.ErrUseLastResponse
    95  		}
    96  	}
    97  
    98  	// set user agent header
    99  	if ystring.IsNotEmpty(opts.UserAgent) {
   100  		cli.headers["User-Agent"] = opts.UserAgent
   101  	}
   102  
   103  	// set auth header, if basic auth or bearer token both exist, use bearer token
   104  	if ystring.IsNotEmpty(opts.Username) || ystring.IsNotEmpty(opts.Password) {
   105  		auth := opts.Username + ":" + opts.Password
   106  		ba := base64.StdEncoding.EncodeToString([]byte(auth))
   107  		cli.headers["Authorization"] = "Basic " + ba
   108  	}
   109  	if ystring.IsNotEmpty(opts.BearerToken) {
   110  		cli.headers["Authorization"] = "Bearer " + opts.BearerToken
   111  	}
   112  
   113  	// merge default headers and user headers
   114  	if opts.Headers != nil {
   115  		for k, v := range opts.Headers {
   116  			cli.headers[k] = v
   117  		}
   118  	}
   119  
   120  	return cli
   121  }
   122  
   123  // Custom performs a custom request with given method, url, query arguments, headers and io.Reader payload as body.
   124  func (c *HTTPClient) Custom(method, url string, queryArgs, headers map[string]string, payload io.Reader) ([]byte, error) {
   125  	return c.sendRequest(method, url, queryArgs, headers, payload)
   126  }
   127  
   128  // Get performs a GET request. It shadows the http.Client.Get method.
   129  func (c *HTTPClient) Get(url string, queryArgs, headers map[string]string) ([]byte, error) {
   130  	return c.sendRequest(http.MethodGet, url, queryArgs, headers, nil)
   131  }
   132  
   133  // GetJSON performs a GET request, parses the JSON-encoded response data and stores in the value pointed to by result.
   134  func (c *HTTPClient) GetJSON(url string, queryArgs, headers map[string]string, result interface{}) ([]byte, error) {
   135  	// set content type to application/json
   136  	if headers == nil {
   137  		headers = make(map[string]string, 1)
   138  	}
   139  	headers["Content-Type"] = "application/json"
   140  	// send request
   141  	resp, err := c.sendRequest(http.MethodGet, url, queryArgs, headers, nil)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  	// unmarshal the response body into result
   146  	if err = json.Unmarshal(resp, result); err != nil {
   147  		return resp, fmt.Errorf("amoy http: response: %w", err)
   148  	}
   149  	return resp, nil
   150  }
   151  
   152  // Post performs a POST request with given io.Reader payload as body. It shadows the http.Client.Post method.
   153  func (c *HTTPClient) Post(url string, queryArgs, headers map[string]string, payload io.Reader) ([]byte, error) {
   154  	return c.sendRequest(http.MethodPost, url, queryArgs, headers, payload)
   155  }
   156  
   157  // PostData performs a POST request with given bytes payload as body.
   158  func (c *HTTPClient) PostData(url string, queryArgs, headers map[string]string, payload []byte) ([]byte, error) {
   159  	return c.sendRequest(http.MethodPost, url, queryArgs, headers, bytes.NewReader(payload))
   160  }
   161  
   162  // PostForm performs a POST request with given form data as body.
   163  func (c *HTTPClient) PostForm(url string, queryArgs, headers map[string]string, form url.Values) ([]byte, error) {
   164  	// set content type to application/x-www-form-urlencoded
   165  	if headers == nil {
   166  		headers = make(map[string]string, 1)
   167  	}
   168  	headers["Content-Type"] = "application/x-www-form-urlencoded"
   169  	// send request
   170  	return c.sendRequest(http.MethodPost, url, queryArgs, headers, strings.NewReader(form.Encode()))
   171  }
   172  
   173  // PostJSON performs a POST request with given JSON payload as body, parses the JSON-encoded response data and stores in the value pointed to by result.
   174  func (c *HTTPClient) PostJSON(url string, queryArgs, headers map[string]string, payload, result interface{}) ([]byte, error) {
   175  	// set content type to application/json
   176  	if headers == nil {
   177  		headers = make(map[string]string, 1)
   178  	}
   179  	headers["Content-Type"] = "application/json"
   180  	// marshal payload
   181  	var body io.Reader
   182  	if !IsInterfaceNil(payload) {
   183  		bs, err := json.Marshal(payload)
   184  		if err != nil {
   185  			return nil, fmt.Errorf("amoy http: request: %w", err)
   186  		}
   187  		body = bytes.NewReader(bs)
   188  	}
   189  	// send request
   190  	resp, err := c.sendRequest(http.MethodPost, url, queryArgs, headers, body)
   191  	if err != nil {
   192  		return nil, err
   193  	}
   194  	// unmarshal the response body into result
   195  	if err = json.Unmarshal(resp, result); err != nil {
   196  		return resp, fmt.Errorf("amoy http: response: %w", err)
   197  	}
   198  	return resp, nil
   199  }
   200  
   201  func (c *HTTPClient) sendRequest(method, url string, queryArgs, headers map[string]string, payload io.Reader) ([]byte, error) {
   202  	// create a new request
   203  	req, err := http.NewRequest(method, url, payload)
   204  	if err != nil {
   205  		return nil, fmt.Errorf("amoy http: request: %w", err)
   206  	}
   207  
   208  	// apply query arguments
   209  	if queryArgs != nil {
   210  		q := req.URL.Query()
   211  		for k, v := range queryArgs {
   212  			q.Add(k, v)
   213  		}
   214  		req.URL.RawQuery = q.Encode()
   215  	}
   216  	// apply default headers
   217  	if c.headers != nil {
   218  		for k, v := range c.headers {
   219  			req.Header.Set(k, v)
   220  		}
   221  	}
   222  	// apply user headers
   223  	if headers != nil {
   224  		for k, v := range headers {
   225  			req.Header.Set(k, v)
   226  		}
   227  	}
   228  
   229  	// send request
   230  	resp, err := c.Client.Do(req)
   231  	if err != nil {
   232  		if IsTimeoutError(err) {
   233  			return nil, fmt.Errorf("amoy http: time out: %v", c.timeout)
   234  		} else if IsNoNetworkError(err) {
   235  			return nil, fmt.Errorf("amoy http: no such host: %w", err)
   236  		} else {
   237  			return nil, fmt.Errorf("amoy http: error: %w", err)
   238  		}
   239  	}
   240  	defer resp.Body.Close()
   241  
   242  	// check response status code
   243  	if !((http.StatusOK <= resp.StatusCode) && (resp.StatusCode < http.StatusMultipleChoices)) {
   244  		return nil, fmt.Errorf("amoy http: status code: %d", resp.StatusCode)
   245  	}
   246  
   247  	// read response body
   248  	body, err := ioutil.ReadAll(resp.Body)
   249  	if err != nil {
   250  		return nil, fmt.Errorf("amoy http: response body: %w", err)
   251  	}
   252  	return body, nil
   253  }
   254  
   255  // PostJSONWithHeaders sends payload in JSON to target URL with given timeout and headers and parses response as JSON.
   256  func PostJSONWithHeaders(url string, dataReq, dataResp interface{}, timeout time.Duration, headers map[string]string) error {
   257  	return postJSON(url, dataReq, dataResp, timeout, headers, EmptyStr, EmptyStr)
   258  }
   259  
   260  // PostJSONAndDumps sends payload in JSON to target URL with given timeout and headers and dumps response and parses response as JSON.
   261  func PostJSONAndDumps(url string, dataReq, dataResp interface{}, timeout time.Duration, headers map[string]string, dumpReqPath, dumpRespPath string) error {
   262  	return postJSON(url, dataReq, dataResp, timeout, headers, dumpReqPath, dumpRespPath)
   263  }
   264  
   265  func postJSON(url string, dataReq, dataResp interface{}, timeout time.Duration, headers map[string]string, dumpReqPath, dumpRespPath string) error {
   266  	var (
   267  		client = http.Client{
   268  			Timeout: timeout,
   269  		}
   270  		req      *http.Request
   271  		resp     *http.Response
   272  		reqJSON  []byte
   273  		respJSON []byte
   274  		err      error
   275  	)
   276  
   277  	// build payload
   278  	if !IsInterfaceNil(dataReq) {
   279  		if reqJSON, err = json.Marshal(dataReq); err != nil {
   280  			return fmt.Errorf("fail to marshal request, error: %w, data: %s", err, dataReq)
   281  		}
   282  	}
   283  
   284  	// build request
   285  	if req, err = http.NewRequest("POST", url, bytes.NewReader(reqJSON)); err != nil {
   286  		return err
   287  	}
   288  	req.Header.Set("Content-Type", "application/json")
   289  	for k, v := range headers {
   290  		req.Header.Set(k, v)
   291  	}
   292  
   293  	// send request
   294  	if resp, err = client.Do(req); err != nil {
   295  		return err
   296  	}
   297  	defer func() {
   298  		_ = resp.Body.Close()
   299  	}()
   300  
   301  	// read response
   302  	if respJSON, err = ioutil.ReadAll(resp.Body); err != nil {
   303  		return err
   304  	}
   305  
   306  	// dump request
   307  	if ystring.IsNotBlank(dumpReqPath) {
   308  		if data, err := httputil.DumpRequest(req, false); err == nil {
   309  			_ = ioutil.WriteFile(dumpReqPath, append(data, reqJSON...), 0644)
   310  		}
   311  	}
   312  
   313  	// dump response
   314  	if ystring.IsNotBlank(dumpRespPath) {
   315  		data, err := httputil.DumpResponse(resp, false)
   316  		if err == nil {
   317  			_ = ioutil.WriteFile(dumpRespPath, append(data, respJSON...), 0644)
   318  		}
   319  	}
   320  
   321  	// check status code
   322  	if !(200 <= resp.StatusCode && resp.StatusCode <= 299) {
   323  		return fmt.Errorf("http error status, code: %d, body: %s", resp.StatusCode, respJSON)
   324  	}
   325  
   326  	// parse response
   327  	if err := json.Unmarshal(respJSON, dataResp); err != nil {
   328  		return fmt.Errorf("fail to unmarshal response, error: %w, body: %s", err, respJSON)
   329  	}
   330  	return nil
   331  }