client.go raw

   1  // Copyright 2021-2023 The sacloud/go-http authors
   2  //
   3  // Licensed under the Apache License, Version 2.0 (the "License");
   4  // you may not use this file except in compliance with the License.
   5  // You may obtain a copy of the License at
   6  //
   7  //      http://www.apache.org/licenses/LICENSE-2.0
   8  //
   9  // Unless required by applicable law or agreed to in writing, software
  10  // distributed under the License is distributed on an "AS IS" BASIS,
  11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12  // See the License for the specific language governing permissions and
  13  // limitations under the License.
  14  
  15  package http
  16  
  17  import (
  18  	"compress/gzip"
  19  	"context"
  20  	"fmt"
  21  	"net/http"
  22  	"runtime"
  23  	"time"
  24  
  25  	"github.com/hashicorp/go-retryablehttp"
  26  )
  27  
  28  var (
  29  	// DefaultUserAgent デフォルトのユーザーエージェント
  30  	DefaultUserAgent = fmt.Sprintf(
  31  		"go-http/v%s (%s/%s; +https://github.com/sacloud/go-http)",
  32  		Version,
  33  		runtime.GOOS,
  34  		runtime.GOARCH,
  35  	)
  36  
  37  	// DefaultAcceptLanguage デフォルトのAcceptLanguage
  38  	DefaultAcceptLanguage = ""
  39  
  40  	// DefaultRetryMax デフォルトのリトライ回数
  41  	DefaultRetryMax = 10
  42  
  43  	// DefaultRetryWaitMin デフォルトのリトライ間隔(最小)
  44  	DefaultRetryWaitMin = 1 * time.Second
  45  
  46  	// DefaultRetryWaitMax デフォルトのリトライ間隔(最大)
  47  	DefaultRetryWaitMax = 64 * time.Second
  48  )
  49  
  50  // Client さくらのクラウドAPI(secure.sakura.ad.jp)向けのHTTPクライアント
  51  //
  52  // レスポンスの状態に応じてリトライする仕組みを持つ
  53  // デフォルトだとレスポンスステータスコード423、または503を受け取った場合にRetryMax回リトライする
  54  //
  55  // リトライ間隔はRetryMinからRetryMaxまで指数的に増加する(Exponential Backoff)
  56  //
  57  // リトライ時にcontext.Canceled、またはcontext.DeadlineExceededの場合はリトライしない
  58  type Client struct {
  59  	// AccessToken アクセストークン
  60  	AccessToken string `validate:"required"`
  61  	// AccessTokenSecret アクセストークンシークレット
  62  	AccessTokenSecret string `validate:"required"`
  63  	// ユーザーエージェント
  64  	UserAgent string
  65  	// Accept-Language
  66  	AcceptLanguage string
  67  	// Gzipを有効にするか
  68  	Gzip bool
  69  	// CheckRetryFunc リトライすべきか判定するためのfunc
  70  	CheckRetryFunc func(ctx context.Context, resp *http.Response, err error) (bool, error)
  71  	// リトライ回数
  72  	RetryMax int
  73  	// リトライ待ち時間(最小)
  74  	RetryWaitMin time.Duration
  75  	// リトライ待ち時間(最大)
  76  	RetryWaitMax time.Duration
  77  	// APIコール時に利用される*http.Client 未指定の場合http.DefaultClientが利用される
  78  	HTTPClient *http.Client
  79  	// RequestCustomizer リクエスト前に*http.Requestのカスタマイズを行うためのfunc
  80  	RequestCustomizer RequestCustomizer
  81  }
  82  
  83  // NewClient APIクライアント作成
  84  func NewClient(token, secret string) *Client {
  85  	c := &Client{
  86  		AccessToken:       token,
  87  		AccessTokenSecret: secret,
  88  	}
  89  	return c
  90  }
  91  
  92  func (c *Client) init() {
  93  	if c.UserAgent == "" {
  94  		c.UserAgent = DefaultUserAgent
  95  	}
  96  	if c.AcceptLanguage == "" {
  97  		c.AcceptLanguage = DefaultAcceptLanguage
  98  	}
  99  	if c.CheckRetryFunc == nil {
 100  		c.CheckRetryFunc = retryablehttp.DefaultRetryPolicy
 101  	}
 102  	if c.RetryMax == 0 {
 103  		c.RetryMax = DefaultRetryMax
 104  	}
 105  	if c.RetryWaitMin == 0 {
 106  		c.RetryWaitMin = DefaultRetryWaitMin
 107  	}
 108  	if c.RetryWaitMax == 0 {
 109  		c.RetryWaitMax = DefaultRetryWaitMax
 110  	}
 111  }
 112  
 113  func (c *Client) httpClient() *retryablehttp.Client {
 114  	return &retryablehttp.Client{
 115  		HTTPClient:   c.HTTPClient,
 116  		RetryWaitMin: c.RetryWaitMin,
 117  		RetryWaitMax: c.RetryWaitMax,
 118  		RetryMax:     c.RetryMax,
 119  		CheckRetry:   c.CheckRetryFunc,
 120  		Backoff:      retryablehttp.DefaultBackoff,
 121  	}
 122  }
 123  
 124  // Do APIコール実施
 125  func (c *Client) Do(req *http.Request) (*http.Response, error) {
 126  	c.init()
 127  
 128  	// set headers
 129  	req.SetBasicAuth(c.AccessToken, c.AccessTokenSecret)
 130  	if req.Header.Get("Content-Type") == "" && req.Body != nil {
 131  		req.Header.Add("Content-Type", "application/json")
 132  	}
 133  	if c.Gzip && req.Header.Get("Accept-Encoding") == "" {
 134  		req.Header.Add("Accept-Encoding", "gzip")
 135  	}
 136  	if req.Header.Get("X-Requested-With") == "" {
 137  		req.Header.Add("X-Requested-With", "XMLHttpRequest")
 138  	}
 139  	if req.Header.Get("X-Sakura-Bigint-As-Int") == "" {
 140  		req.Header.Add("X-Sakura-Bigint-As-Int", "1") // Use BigInt on resource ids.
 141  	}
 142  	if req.Header.Get("User-Agent") == "" {
 143  		req.Header.Add("User-Agent", c.UserAgent)
 144  	}
 145  	if req.Header.Get("Accept-Language") == "" && c.AcceptLanguage != "" {
 146  		req.Header.Add("Accept-Language", c.AcceptLanguage)
 147  	}
 148  
 149  	if c.RequestCustomizer != nil {
 150  		if err := c.RequestCustomizer(req); err != nil {
 151  			return nil, err
 152  		}
 153  	}
 154  
 155  	request, err := retryablehttp.FromRequest(req)
 156  	if err != nil {
 157  		return nil, err
 158  	}
 159  
 160  	client := c.httpClient()
 161  
 162  	// API call
 163  	resp, err := client.Do(request)
 164  	if err != nil {
 165  		return nil, err
 166  	}
 167  
 168  	if c.Gzip && resp.Header.Get("Content-Encoding") == "gzip" {
 169  		body, err := gzip.NewReader(resp.Body)
 170  		if err != nil {
 171  			return nil, err
 172  		}
 173  		resp.Body = body
 174  	}
 175  
 176  	return resp, err
 177  }
 178