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