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