github.com/pingcap/tiflow@v0.0.0-20240520035814-5bf52d54e205/pkg/api/internal/rest/request.go (about) 1 // Copyright 2021 PingCAP, Inc. 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 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package rest 15 16 import ( 17 "bytes" 18 "context" 19 "encoding/json" 20 "errors" 21 "fmt" 22 "io" 23 "net/http" 24 "net/url" 25 "path" 26 "time" 27 28 "github.com/pingcap/log" 29 "github.com/pingcap/tiflow/cdc/api/middleware" 30 "github.com/pingcap/tiflow/cdc/model" 31 cerrors "github.com/pingcap/tiflow/pkg/errors" 32 "github.com/pingcap/tiflow/pkg/httputil" 33 "github.com/pingcap/tiflow/pkg/retry" 34 "github.com/pingcap/tiflow/pkg/version" 35 "go.uber.org/zap" 36 ) 37 38 const ( 39 defaultBackoffBaseDelayInMs = 500 40 defaultBackoffMaxDelayInMs = 30 * 1000 41 // The write operations other than 'GET' are not idempotent, 42 // so we only try one time in request.Do() and let users specify the retrying behaviour 43 defaultMaxRetries = 1 44 ) 45 46 // Request allows for building up a request to cdc server in a chained fasion. 47 // Any errors are stored until the end of your call, so you only have to 48 // check once. 49 type Request struct { 50 c *CDCRESTClient 51 timeout time.Duration 52 53 // generic components accessible via setters 54 method HTTPMethod 55 pathPrefix string 56 params url.Values 57 headers http.Header 58 basicAuth BasicAuth 59 60 // retry options 61 backoffBaseDelay time.Duration 62 backoffMaxDelay time.Duration 63 maxRetries uint64 64 65 // output 66 err error 67 body io.Reader 68 } 69 70 // NewRequest creates a new request. 71 func NewRequest(c *CDCRESTClient) *Request { 72 var pathPrefix string 73 if c.base != nil { 74 pathPrefix = path.Join("/", c.base.Path, c.versionedAPIPath) 75 } else { 76 pathPrefix = path.Join("/", c.versionedAPIPath) 77 } 78 79 var timeout time.Duration 80 if c.Client != nil { 81 timeout = c.Client.Timeout() 82 } 83 84 r := &Request{ 85 c: c, 86 timeout: timeout, 87 pathPrefix: pathPrefix, 88 maxRetries: 1, 89 params: c.params, 90 basicAuth: c.basicAuth, 91 } 92 r.WithHeader("Accept", "application/json") 93 r.WithHeader(middleware.ClientVersionHeader, version.ReleaseVersion) 94 return r 95 } 96 97 // newRequestWithClient creates a Request with an embedded CDCRESTClient for test. 98 func newRequestWithClient(base *url.URL, versionedAPIPath string, client *httputil.Client) *Request { 99 return NewRequest(&CDCRESTClient{ 100 base: base, 101 versionedAPIPath: versionedAPIPath, 102 Client: client, 103 }) 104 } 105 106 // WithPrefix adds segments to the beginning of request url. 107 func (r *Request) WithPrefix(segments ...string) *Request { 108 if r.err != nil { 109 return r 110 } 111 112 r.pathPrefix = path.Join(r.pathPrefix, path.Join(segments...)) 113 return r 114 } 115 116 // WithURI sets the server relative URI. 117 func (r *Request) WithURI(uri string) *Request { 118 if r.err != nil { 119 return r 120 } 121 u, err := url.Parse(uri) 122 if err != nil { 123 r.err = err 124 return r 125 } 126 r.pathPrefix = path.Join(r.pathPrefix, u.Path) 127 vals := u.Query() 128 if len(vals) > 0 { 129 if r.params == nil { 130 r.params = make(url.Values) 131 } 132 for k, v := range vals { 133 r.params[k] = v 134 } 135 } 136 return r 137 } 138 139 // WithParam sets the http request query params. 140 func (r *Request) WithParam(name, value string) *Request { 141 if r.err != nil { 142 return r 143 } 144 if r.params == nil { 145 r.params = make(url.Values) 146 } 147 r.params[name] = append(r.params[name], value) 148 return r 149 } 150 151 // WithMethod sets the method this request will use. 152 func (r *Request) WithMethod(method HTTPMethod) *Request { 153 r.method = method 154 return r 155 } 156 157 // WithHeader set the http request header. 158 func (r *Request) WithHeader(key string, values ...string) *Request { 159 if r.headers == nil { 160 r.headers = http.Header{} 161 } 162 r.headers.Del(key) 163 for _, value := range values { 164 r.headers.Add(key, value) 165 } 166 return r 167 } 168 169 // WithTimeout specifies overall timeout of a request. 170 func (r *Request) WithTimeout(d time.Duration) *Request { 171 if r.err != nil { 172 return r 173 } 174 r.timeout = d 175 return r 176 } 177 178 // WithBackoffBaseDelay specifies the base backoff sleep duration. 179 func (r *Request) WithBackoffBaseDelay(delay time.Duration) *Request { 180 if r.err != nil { 181 return r 182 } 183 r.backoffBaseDelay = delay 184 return r 185 } 186 187 // WithBackoffMaxDelay specifies the maximum backoff sleep duration. 188 func (r *Request) WithBackoffMaxDelay(delay time.Duration) *Request { 189 if r.err != nil { 190 return r 191 } 192 r.backoffMaxDelay = delay 193 return r 194 } 195 196 // WithMaxRetries specifies the maximum times a request will retry. 197 func (r *Request) WithMaxRetries(maxRetries uint64) *Request { 198 if r.err != nil { 199 return r 200 } 201 if maxRetries > 0 { 202 r.maxRetries = maxRetries 203 } else { 204 r.maxRetries = defaultMaxRetries 205 } 206 return r 207 } 208 209 // WithBody makes http request use obj as its body. 210 // only supports two types now: 211 // 1. io.Reader 212 // 2. type which can be json marshalled 213 func (r *Request) WithBody(obj interface{}) *Request { 214 if r.err != nil { 215 return r 216 } 217 218 if rd, ok := obj.(io.Reader); ok { 219 r.body = rd 220 } else { 221 b, err := json.Marshal(obj) 222 if err != nil { 223 r.err = err 224 return r 225 } 226 r.body = bytes.NewReader(b) 227 r.WithHeader("Content-Type", "application/json") 228 } 229 return r 230 } 231 232 // URL returns the current working URL. 233 func (r *Request) URL() *url.URL { 234 p := r.pathPrefix 235 236 finalURL := &url.URL{} 237 if r.c.base != nil { 238 *finalURL = *r.c.base 239 } 240 finalURL.Path = p 241 242 query := url.Values{} 243 for key, values := range r.params { 244 for _, value := range values { 245 query.Add(key, value) 246 } 247 } 248 249 finalURL.RawQuery = query.Encode() 250 return finalURL 251 } 252 253 func (r *Request) newHTTPRequest(ctx context.Context) (*http.Request, error) { 254 url := r.URL().String() 255 req, err := http.NewRequest(r.method.String(), url, r.body) 256 if err != nil { 257 return nil, err 258 } 259 req = req.WithContext(ctx) 260 req.Header = r.headers 261 req.SetBasicAuth(r.basicAuth.User, r.basicAuth.Password) 262 return req, nil 263 } 264 265 // Do formats and executes the request. 266 func (r *Request) Do(ctx context.Context) (res *Result) { 267 if r.err != nil { 268 log.Info("error in request", zap.Error(r.err)) 269 return &Result{err: r.err} 270 } 271 272 client := r.c.Client 273 if client == nil { 274 client = &httputil.Client{} 275 } 276 277 if r.timeout > 0 { 278 var cancel context.CancelFunc 279 ctx, cancel = context.WithTimeout(ctx, r.timeout) 280 defer cancel() 281 } 282 283 baseDelay := r.backoffBaseDelay.Milliseconds() 284 if baseDelay == 0 { 285 baseDelay = defaultBackoffBaseDelayInMs 286 } 287 maxDelay := r.backoffMaxDelay.Milliseconds() 288 if maxDelay == 0 { 289 maxDelay = defaultBackoffMaxDelayInMs 290 } 291 maxRetries := r.maxRetries 292 if maxRetries <= 0 { 293 maxRetries = defaultMaxRetries 294 } 295 296 fn := func() error { 297 req, err := r.newHTTPRequest(ctx) 298 if err != nil { 299 return err 300 } 301 // rewind the request body when r.body is not nil 302 if seeker, ok := r.body.(io.Seeker); ok && r.body != nil { 303 if _, err := seeker.Seek(0, 0); err != nil { 304 return cerrors.ErrRewindRequestBodyError 305 } 306 } 307 308 resp, err := client.Do(req) 309 if err != nil { 310 log.Error("failed to send a http request", zap.Error(err)) 311 return err 312 } 313 314 defer func() { 315 if resp == nil { 316 return 317 } 318 // close the body to let the TCP connection be reused after reconnecting 319 // see https://github.com/golang/go/blob/go1.18.1/src/net/http/response.go#L62-L64 320 _, _ = io.Copy(io.Discard, resp.Body) 321 resp.Body.Close() 322 }() 323 324 res = r.checkResponse(resp) 325 if res.Error() != nil { 326 return res.Error() 327 } 328 return nil 329 } 330 331 var err error 332 if maxRetries > 1 { 333 err = retry.Do(ctx, fn, 334 retry.WithBackoffBaseDelay(baseDelay), 335 retry.WithBackoffMaxDelay(maxDelay), 336 retry.WithMaxTries(maxRetries), 337 retry.WithIsRetryableErr(cerrors.IsRetryableError), 338 ) 339 } else { 340 err = fn() 341 } 342 343 if res == nil && err != nil { 344 return &Result{err: err} 345 } 346 347 return 348 } 349 350 // check http response and unmarshal error message if necessary. 351 func (r *Request) checkResponse(resp *http.Response) *Result { 352 var body []byte 353 if resp.Body != nil { 354 data, err := io.ReadAll(resp.Body) 355 if err != nil { 356 return &Result{err: err} 357 } 358 body = data 359 } 360 361 contentType := resp.Header.Get("Content-Type") 362 if resp.StatusCode < http.StatusOK || resp.StatusCode > http.StatusPartialContent { 363 var jsonErr model.HTTPError 364 err := json.Unmarshal(body, &jsonErr) 365 if err == nil { 366 err = errors.New(jsonErr.Error) 367 } else { 368 err = fmt.Errorf( 369 "call cdc api failed, url=%s, "+ 370 "code=%d, contentType=%s, response=%s", 371 r.URL().String(), 372 resp.StatusCode, contentType, string(body)) 373 } 374 375 return &Result{ 376 body: body, 377 contentType: contentType, 378 statusCode: resp.StatusCode, 379 err: err, 380 } 381 } 382 383 return &Result{ 384 body: body, 385 contentType: contentType, 386 statusCode: resp.StatusCode, 387 } 388 } 389 390 // Result contains the result of calling Request.Do(). 391 type Result struct { 392 body []byte 393 contentType string 394 err error 395 statusCode int 396 } 397 398 // Raw returns the raw result. 399 func (r Result) Raw() ([]byte, error) { 400 return r.body, r.err 401 } 402 403 // Error returns the request error. 404 func (r Result) Error() error { 405 return r.err 406 } 407 408 // Into stores the http response body into obj. 409 func (r Result) Into(obj interface{}) error { 410 if r.err != nil { 411 return r.err 412 } 413 414 if len(r.body) == 0 { 415 return cerrors.ErrZeroLengthResponseBody.GenWithStackByArgs(r.statusCode) 416 } 417 418 return json.Unmarshal(r.body, obj) 419 }