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