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  }