httputil/httputil.go (165 lines of code) (raw):
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license.
package httputil
import (
"bytes"
"crypto/tls"
"github.com/Azure/azure-extension-foundation/errorhelper"
"io/ioutil"
"log"
"net/http"
"time"
)
const (
OperationGet = "GET"
OperationPost = "POST"
OperationDelete = "DELETE"
OperationPut = "PUT"
)
type HttpClient interface {
Get(url string, headers map[string]string) (responseCode int, body []byte, err error)
Post(url string, headers map[string]string, payload []byte) (responseCode int, body []byte, err error)
Put(url string, headers map[string]string, payload []byte) (responseCode int, body []byte, err error)
Delete(url string, headers map[string]string, payload []byte) (responseCode int, body []byte, err error)
}
// for testing
type httpClientInterface interface {
Do(req *http.Request) (*http.Response, error)
}
type Client struct {
httpClient httpClientInterface
retryBehavior RetryBehavior
}
type RetryBehavior = func(statusCode int, i int) bool
// return false to end retries
// i starts from 1 keeps getting incremented while function returns true
var NoRetry RetryBehavior = func(statusCode int, i int) bool {
return false
}
var LinearRetryThrice RetryBehavior = func(statusCode int, i int) bool {
if !isTransientHttpStatusCode(statusCode) {
return false
}
time.Sleep(time.Second * 3)
if i < 3 {
return true // retry if count < 3
}
return false
}
// The default retry behavior is 5 retries with exponential back-off with a maximum wait time of 60 seconds
var DefaultRetryBehavior RetryBehavior = func(statusCode int, i int) bool {
if !isTransientHttpStatusCode(statusCode) {
return false
}
delay := time.Second * time.Duration(2^(i))
const maxDelay time.Duration = 60 * time.Second
if delay > maxDelay {
delay = maxDelay
}
time.Sleep(delay)
if i < 5 {
return true
}
return false
}
func isTransientHttpStatusCode(statusCode int) bool {
switch statusCode {
case
http.StatusRequestTimeout, // 408
http.StatusTooManyRequests, // 429
http.StatusInternalServerError, // 500
http.StatusBadGateway, // 502
http.StatusServiceUnavailable, // 503
http.StatusGatewayTimeout: // 504
return true // timeout and too many requests
default:
return false
}
}
func IsSuccessStatusCode(statusCode int) bool {
switch statusCode {
case 200, 201:
return true
default:
return false
}
}
func NewSecureHttpClient(retryBehavior RetryBehavior) HttpClient {
if retryBehavior == nil {
panic("Retry policy must be specified")
}
tlsConfig := &tls.Config{
Renegotiation: tls.RenegotiateFreelyAsClient,
}
transport := &http.Transport{TLSClientConfig: tlsConfig}
httpClient := &http.Client{Transport: transport}
return &Client{httpClient, retryBehavior}
}
func NewSecureHttpClientWithCertificates(certificate string, key string, retryBehavior RetryBehavior) HttpClient {
if retryBehavior == nil {
panic("Retry policy must be specified")
}
cert, err := tls.LoadX509KeyPair(certificate, key)
if err != nil {
log.Fatal(err)
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
Renegotiation: tls.RenegotiateFreelyAsClient,
}
transport := &http.Transport{TLSClientConfig: tlsConfig}
httpClient := &http.Client{Transport: transport}
return &Client{httpClient, retryBehavior}
}
func NewInsecureHttpClientWithCertificates(certificate string, key string, retryBehavior RetryBehavior) HttpClient {
if retryBehavior == nil {
panic("Retry policy must be specified")
}
cert, err := tls.LoadX509KeyPair(certificate, key)
if err != nil {
log.Fatal(err)
}
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
InsecureSkipVerify: true,
Renegotiation: tls.RenegotiateFreelyAsClient,
}
transport := &http.Transport{TLSClientConfig: tlsConfig}
httpClient := &http.Client{Transport: transport}
return &Client{httpClient, retryBehavior}
}
// Get issues a get request
func (client *Client) Get(url string, headers map[string]string) (responseCode int, body []byte, err error) {
return client.issueRequest(OperationGet, url, headers, nil)
}
// Post issues a post request
func (client *Client) Post(url string, headers map[string]string, payload []byte) (responseCode int, body []byte, err error) {
return client.issueRequest(OperationPost, url, headers, bytes.NewBuffer(payload))
}
// Put issues a put request
func (client *Client) Put(url string, headers map[string]string, payload []byte) (responseCode int, body []byte, err error) {
return client.issueRequest(OperationPut, url, headers, bytes.NewBuffer(payload))
}
// Delete issues a delete request
func (client *Client) Delete(url string, headers map[string]string, payload []byte) (responseCode int, body []byte, err error) {
return client.issueRequest(OperationDelete, url, headers, bytes.NewBuffer(payload))
}
func (client *Client) issueRequest(operation string, url string, headers map[string]string, payload *bytes.Buffer) (int, []byte, error) {
request, err := http.NewRequest(operation, url, nil)
if payload != nil && payload.Len() != 0 {
request, err = http.NewRequest(operation, url, payload)
}
for key, value := range headers {
request.Header.Add(key, value)
}
res, err := client.httpClient.Do(request)
if err == nil && IsSuccessStatusCode(res.StatusCode) {
// no need to retry
} else if err == nil && res != nil {
// there was no error, so look at the status code to retry
for i := 1; client.retryBehavior(res.StatusCode, i); i++ {
res, err = client.httpClient.Do(request)
if err != nil {
break
}
}
}
if err != nil {
return -1, nil, errorhelper.AddStackToError(err)
}
body, err := ioutil.ReadAll(res.Body)
res.Body.Close()
code := res.StatusCode
if err != nil {
return -1, nil, errorhelper.AddStackToError(err)
}
return code, body, nil
}