factory.go raw

   1  // Copyright 2022-2025 The sacloud/api-client-go 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 client
  16  
  17  import (
  18  	"context"
  19  	"net/http"
  20  	"sync"
  21  	"time"
  22  
  23  	"github.com/hashicorp/go-retryablehttp"
  24  	sacloudhttp "github.com/sacloud/go-http"
  25  )
  26  
  27  // Factory client.HttpRequestDoerを作成して返すファクトリー
  28  type Factory struct {
  29  	options    *Options
  30  	httpClient *http.Client // Transportの初期化を1度だけ行うためにoptionsのHttpClientの参照をここにコピーして保持しておく
  31  
  32  	once sync.Once
  33  }
  34  
  35  // NewFactory 指定のオプションでFactoryを生成する
  36  func NewFactory(options ...*Options) *Factory {
  37  	var opts *Options
  38  	if len(options) > 0 {
  39  		opts = MergeOptions(options...)
  40  	}
  41  	if opts == nil {
  42  		panic("options is nil")
  43  	}
  44  
  45  	return &Factory{
  46  		options:    opts,
  47  		httpClient: opts.HttpClient,
  48  	}
  49  }
  50  
  51  // NewHttpRequestDoer オプションを反映したsacloud向けのHTTPクライアントを生成して返す
  52  func (f *Factory) NewHttpRequestDoer() HttpRequestDoer {
  53  	f.init()
  54  
  55  	ua := f.options.UserAgent
  56  	if ua == "" {
  57  		ua = DefaultUserAgent
  58  	}
  59  	return &sacloudhttp.Client{
  60  		AccessToken:       f.options.AccessToken,
  61  		AccessTokenSecret: f.options.AccessTokenSecret,
  62  		UserAgent:         ua,
  63  		AcceptLanguage:    f.options.AcceptLanguage,
  64  		Gzip:              f.options.Gzip,
  65  		CheckRetryFunc:    f.checkRetryFn(),
  66  		RetryMax:          f.options.RetryMax,
  67  		RetryWaitMin:      time.Duration(f.options.RetryWaitMin) * time.Second,
  68  		RetryWaitMax:      time.Duration(f.options.RetryWaitMax) * time.Second,
  69  		HTTPClient:        f.httpClient,
  70  		RequestCustomizer: sacloudhttp.ComposeRequestCustomizer(f.options.RequestCustomizers...),
  71  	}
  72  }
  73  
  74  // Options Doerの生成で用いるOptionsを返す
  75  func (f *Factory) Options() *Options {
  76  	return f.options
  77  }
  78  
  79  func (f *Factory) init() {
  80  	f.once.Do(func() {
  81  		if f.httpClient == nil {
  82  			f.httpClient = http.DefaultClient
  83  		}
  84  
  85  		timeout := f.options.HttpRequestTimeout
  86  		if timeout == 0 {
  87  			timeout = 300
  88  		}
  89  		f.httpClient.Timeout = time.Duration(timeout) * time.Second
  90  
  91  		rateLimit := f.options.HttpRequestRateLimit
  92  		if rateLimit == 0 {
  93  			rateLimit = 10
  94  		}
  95  		f.httpClient.Transport = &sacloudhttp.RateLimitRoundTripper{
  96  			Transport:       f.httpClient.Transport,
  97  			RateLimitPerSec: rateLimit,
  98  		}
  99  
 100  		if f.options.Trace {
 101  			f.httpClient.Transport = &sacloudhttp.TracingRoundTripper{
 102  				Transport:       f.httpClient.Transport,
 103  				OutputOnlyError: f.options.TraceOnlyError,
 104  			}
 105  		}
 106  	})
 107  }
 108  
 109  func (f *Factory) checkRetryFn() func(ctx context.Context, resp *http.Response, err error) (bool, error) {
 110  	checkRetryFn := retryablehttp.DefaultRetryPolicy
 111  	if len(f.options.CheckRetryStatusCodes) > 0 {
 112  		checkRetryFn = func(ctx context.Context, resp *http.Response, err error) (bool, error) {
 113  			if ctx.Err() != nil {
 114  				return false, ctx.Err()
 115  			}
 116  			if err != nil {
 117  				return retryablehttp.DefaultRetryPolicy(ctx, resp, err)
 118  			}
 119  			if resp.StatusCode == 0 {
 120  				return true, nil
 121  			}
 122  			for _, status := range f.options.CheckRetryStatusCodes {
 123  				if resp.StatusCode == status {
 124  					return true, nil
 125  				}
 126  			}
 127  			return false, nil
 128  		}
 129  	}
 130  	if f.options.CheckRetryFunc != nil {
 131  		checkRetryFn = f.options.CheckRetryFunc
 132  	}
 133  
 134  	return checkRetryFn
 135  }
 136