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 }