github.com/NVIDIA/aistore@v1.3.23-0.20240517131212-7df6609be51d/api/client.go (about)

     1  // Package api provides native Go-based API/SDK over HTTP(S).
     2  /*
     3   * Copyright (c) 2018-2024, NVIDIA CORPORATION. All rights reserved.
     4   */
     5  package api
     6  
     7  import (
     8  	"bytes"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"net/http"
    13  	"net/url"
    14  	"sync"
    15  
    16  	"github.com/NVIDIA/aistore/api/apc"
    17  	"github.com/NVIDIA/aistore/cmn"
    18  	"github.com/NVIDIA/aistore/cmn/cos"
    19  	"github.com/NVIDIA/aistore/cmn/debug"
    20  	jsoniter "github.com/json-iterator/go"
    21  	"github.com/tinylib/msgp/msgp"
    22  )
    23  
    24  const (
    25  	errNilCksum     = "nil checksum"
    26  	errNilCksumType = "checksum is empty (checksum type %q) - cannot validate"
    27  )
    28  
    29  type (
    30  	BaseParams struct {
    31  		Client *http.Client
    32  		URL    string
    33  		Method string
    34  		Token  string
    35  		UA     string
    36  	}
    37  
    38  	// ReqParams is used in constructing client-side API requests to aistore.
    39  	// Stores Query and Headers for providing arguments that are not used commonly in API requests
    40  	//  See also: cmn.HreqArgs
    41  	ReqParams struct {
    42  		Query      url.Values
    43  		Header     http.Header
    44  		BaseParams BaseParams
    45  		Path       string
    46  
    47  		// Authentication
    48  		User     string
    49  		Password string
    50  
    51  		// amsg, lsmsg etc.
    52  		Body []byte
    53  
    54  		// mem-pool (when cos.HdrContentType = cos.ContentMsgPack)
    55  		buf []byte
    56  	}
    57  )
    58  
    59  type (
    60  	reqResp struct {
    61  		client *http.Client
    62  		req    *http.Request
    63  		resp   *http.Response
    64  	}
    65  	wrappedResp struct {
    66  		*http.Response
    67  		cksumValue string // checksum value of the response
    68  		n          int64  // number bytes read from `resp.Body`
    69  	}
    70  )
    71  
    72  func newErrCreateHTTPRequest(err error) error {
    73  	return fmt.Errorf("failed to create http request: %w", err)
    74  }
    75  
    76  // HTTPStatus returns HTTP status or (-1) for non-HTTP error.
    77  func HTTPStatus(err error) int {
    78  	if err == nil {
    79  		return http.StatusOK
    80  	}
    81  	if herr := cmn.Err2HTTPErr(err); herr != nil {
    82  		return herr.Status
    83  	}
    84  	return -1 // invalid
    85  }
    86  
    87  func SetAuxHeaders(r *http.Request, bp *BaseParams) {
    88  	if bp.Token != "" {
    89  		r.Header.Set(apc.HdrAuthorization, apc.AuthenticationTypeBearer+" "+bp.Token)
    90  	}
    91  	if bp.UA != "" {
    92  		r.Header.Set(cos.HdrUserAgent, bp.UA)
    93  	}
    94  }
    95  
    96  func GetWhatRawQuery(getWhat, getProps string) string {
    97  	q := url.Values{}
    98  	q.Add(apc.QparamWhat, getWhat)
    99  	if getProps != "" {
   100  		q.Add(apc.QparamProps, getProps)
   101  	}
   102  	return q.Encode()
   103  }
   104  
   105  //
   106  // do request ------------------------------------------------------------------------------
   107  //
   108  
   109  // uses do() to make the request; if successful, checks, drains, and closes the response body
   110  func (reqParams *ReqParams) DoRequest() error {
   111  	resp, err := reqParams.do()
   112  	if err != nil {
   113  		return err
   114  	}
   115  	return reqParams.cdc(resp)
   116  }
   117  
   118  // same as above except that it also returns response header
   119  func (reqParams *ReqParams) doReqHdr() (_ http.Header, status int, _ error) {
   120  	resp, err := reqParams.do()
   121  	if err == nil {
   122  		return resp.Header, resp.StatusCode, reqParams.cdc(resp)
   123  	}
   124  	if resp != nil {
   125  		status = resp.StatusCode
   126  	}
   127  	return nil, status, err
   128  }
   129  
   130  // Makes request via do(), decodes `resp.Body` into the `out` structure,
   131  // closes the former, and returns the entire wrapped response
   132  // (as well as `out`)
   133  //
   134  // Returns an error if the response status >= 400.
   135  func (reqParams *ReqParams) DoReqAny(out any) (int, error) {
   136  	debug.AssertNotPstr(out)
   137  	resp, err := reqParams.do()
   138  	if err != nil {
   139  		return 0, err
   140  	}
   141  	err = reqParams.readAny(resp, out)
   142  	cos.DrainReader(resp.Body)
   143  	resp.Body.Close()
   144  	return resp.StatusCode, err
   145  }
   146  
   147  // same as above with `out` being a string
   148  func (reqParams *ReqParams) doReqStr(out *string) (int, error) {
   149  	resp, err := reqParams.do()
   150  	if err != nil {
   151  		return 0, err
   152  	}
   153  	err = reqParams.readStr(resp, out)
   154  	cos.DrainReader(resp.Body)
   155  	resp.Body.Close()
   156  	return resp.StatusCode, err
   157  }
   158  
   159  // Makes request via do() and uses provided writer to write `resp.Body`
   160  // (which is also closes)
   161  //
   162  // Returns the entire wrapped response.
   163  func (reqParams *ReqParams) doWriter(w io.Writer) (wresp *wrappedResp, err error) {
   164  	var resp *http.Response
   165  	resp, err = reqParams.do()
   166  	if err != nil {
   167  		return
   168  	}
   169  	wresp, err = reqParams.rwResp(resp, w)
   170  	cos.DrainReader(resp.Body)
   171  	resp.Body.Close()
   172  	return
   173  }
   174  
   175  // same as above except that it returns response body (as io.ReadCloser) for subsequent reading
   176  func (reqParams *ReqParams) doReader() (io.ReadCloser, int64, error) {
   177  	resp, err := reqParams.do()
   178  	if err != nil {
   179  		return nil, 0, err
   180  	}
   181  	if err := reqParams.checkResp(resp); err != nil {
   182  		resp.Body.Close()
   183  		return nil, 0, err
   184  	}
   185  	return resp.Body, resp.ContentLength, nil
   186  }
   187  
   188  // makes HTTP request, retries on connection-refused and reset errors, and returns the response
   189  func (reqParams *ReqParams) do() (resp *http.Response, err error) {
   190  	var reqBody io.Reader
   191  	if reqParams.Body != nil {
   192  		reqBody = bytes.NewBuffer(reqParams.Body)
   193  	}
   194  	urlPath := reqParams.BaseParams.URL + reqParams.Path
   195  	req, errR := http.NewRequest(reqParams.BaseParams.Method, urlPath, reqBody)
   196  	if errR != nil {
   197  		return nil, fmt.Errorf("failed to create http request: %w", errR)
   198  	}
   199  	reqParams.setRequestOptParams(req)
   200  	SetAuxHeaders(req, &reqParams.BaseParams)
   201  
   202  	rr := reqResp{client: reqParams.BaseParams.Client, req: req}
   203  	err = cmn.NetworkCallWithRetry(&cmn.RetryArgs{
   204  		Call:      rr.call,
   205  		Verbosity: cmn.RetryLogOff,
   206  		SoftErr:   httpMaxRetries,
   207  		Sleep:     httpRetrySleep,
   208  		BackOff:   true,
   209  		IsClient:  true,
   210  	})
   211  	resp = rr.resp
   212  	if err == nil {
   213  		return resp, nil
   214  	}
   215  	if resp != nil {
   216  		herr := cmn.NewErrHTTP(req, err, resp.StatusCode)
   217  		herr.Method, herr.URLPath = reqParams.BaseParams.Method, reqParams.Path
   218  		return nil, herr
   219  	}
   220  	if uerr, ok := err.(*url.Error); ok {
   221  		err = uerr.Unwrap()
   222  		herr := cmn.NewErrHTTP(req, err, 0)
   223  		herr.Method, herr.URLPath = reqParams.BaseParams.Method, reqParams.Path
   224  		return nil, herr
   225  	}
   226  	return nil, err
   227  }
   228  
   229  // Check, Drain, Close
   230  func (reqParams *ReqParams) cdc(resp *http.Response) (err error) {
   231  	err = reqParams.checkResp(resp)
   232  	cos.DrainReader(resp.Body)
   233  	resp.Body.Close() // ignore Close err, if any
   234  	return
   235  }
   236  
   237  // setRequestOptParams given an existing HTTP Request and optional API parameters,
   238  // sets the optional fields of the request if provided.
   239  func (reqParams *ReqParams) setRequestOptParams(req *http.Request) {
   240  	if len(reqParams.Query) != 0 {
   241  		req.URL.RawQuery = reqParams.Query.Encode()
   242  	}
   243  	if reqParams.Header != nil {
   244  		req.Header = reqParams.Header
   245  	}
   246  	if reqParams.User != "" && reqParams.Password != "" {
   247  		req.SetBasicAuth(reqParams.User, reqParams.Password)
   248  	}
   249  }
   250  
   251  //
   252  // check, read, write, validate http.Response ----------------------------------------------
   253  //
   254  
   255  // decode response iff: err == nil AND status in (ok, partial-content)
   256  func (reqParams *ReqParams) readAny(resp *http.Response, out any) (err error) {
   257  	debug.Assert(out != nil)
   258  	if err = reqParams.checkResp(resp); err != nil {
   259  		return
   260  	}
   261  	if code := resp.StatusCode; code != http.StatusOK && code != http.StatusPartialContent {
   262  		return
   263  	}
   264  	// json or msgpack
   265  	if resp.Header.Get(cos.HdrContentType) == cos.ContentMsgPack {
   266  		debug.Assert(cap(reqParams.buf) > cos.KiB) // caller must allocate
   267  		r := msgp.NewReaderBuf(resp.Body, reqParams.buf)
   268  		err = out.(msgp.Decodable).DecodeMsg(r)
   269  	} else {
   270  		err = jsoniter.NewDecoder(resp.Body).Decode(out)
   271  	}
   272  	if err != nil {
   273  		err = fmt.Errorf("unexpected: failed to decode response: %v -> %T", err, out)
   274  	}
   275  	return
   276  }
   277  
   278  func (reqParams *ReqParams) readStr(resp *http.Response, out *string) error {
   279  	if err := reqParams.checkResp(resp); err != nil {
   280  		return err
   281  	}
   282  	b, err := io.ReadAll(resp.Body)
   283  	if err != nil {
   284  		return fmt.Errorf("failed to read response: %w", err)
   285  	}
   286  	*out = string(b)
   287  	return nil
   288  }
   289  
   290  func (reqParams *ReqParams) rwResp(resp *http.Response, w io.Writer) (*wrappedResp, error) {
   291  	if err := reqParams.checkResp(resp); err != nil {
   292  		return nil, err
   293  	}
   294  	wresp := &wrappedResp{Response: resp}
   295  	n, err := io.Copy(w, resp.Body)
   296  	if err != nil {
   297  		return nil, err
   298  	}
   299  	// NOTE: Content-Length == -1 (unknown) for transformed objects
   300  	debug.Assertf(n == resp.ContentLength || resp.ContentLength == -1, "%d vs %d", n, wresp.n)
   301  	wresp.n = n
   302  	return wresp, nil
   303  }
   304  
   305  // end-to-end protection (compare w/ rwResp above)
   306  func (reqParams *ReqParams) readValidate(resp *http.Response, w io.Writer) (*wrappedResp, error) {
   307  	var (
   308  		wresp     = &wrappedResp{Response: resp, n: resp.ContentLength}
   309  		cksumType = resp.Header.Get(apc.HdrObjCksumType)
   310  	)
   311  	if err := reqParams.checkResp(resp); err != nil {
   312  		return nil, err
   313  	}
   314  	n, cksum, err := cos.CopyAndChecksum(w, resp.Body, nil, cksumType)
   315  	if err != nil {
   316  		return nil, err
   317  	}
   318  	if n != resp.ContentLength {
   319  		return nil, fmt.Errorf("read length (%d) != (%d) content-length", n, resp.ContentLength)
   320  	}
   321  	if cksum == nil {
   322  		if cksumType == "" {
   323  			return nil, errors.New(errNilCksum) // e.g., after fast-appending to a TAR
   324  		}
   325  		return nil, fmt.Errorf(errNilCksumType, cksumType)
   326  	}
   327  
   328  	// compare
   329  	wresp.cksumValue = cksum.Value()
   330  	hdrCksumValue := wresp.Header.Get(apc.HdrObjCksumVal)
   331  	if wresp.cksumValue != hdrCksumValue {
   332  		return nil, cmn.NewErrInvalidCksum(hdrCksumValue, wresp.cksumValue)
   333  	}
   334  	return wresp, nil
   335  }
   336  
   337  func (reqParams *ReqParams) checkResp(resp *http.Response) error {
   338  	if resp.StatusCode < http.StatusBadRequest {
   339  		return nil
   340  	}
   341  	if reqParams.BaseParams.Method == http.MethodHead {
   342  		// HEAD request does not return body
   343  		if msg := resp.Header.Get(apc.HdrError); msg != "" {
   344  			return &cmn.ErrHTTP{
   345  				TypeCode: cmn.TypeCodeHTTPErr(msg),
   346  				Message:  msg,
   347  				Status:   resp.StatusCode,
   348  				Method:   reqParams.BaseParams.Method,
   349  				URLPath:  reqParams.Path,
   350  			}
   351  		}
   352  	}
   353  
   354  	b, _ := io.ReadAll(resp.Body)
   355  	if len(b) == 0 {
   356  		if resp.StatusCode == http.StatusServiceUnavailable {
   357  			msg := fmt.Sprintf("[%s]: starting up, please try again later...", http.StatusText(http.StatusServiceUnavailable))
   358  			return &cmn.ErrHTTP{Message: msg, Status: resp.StatusCode}
   359  		}
   360  		return &cmn.ErrHTTP{
   361  			Message: "failed to execute " + reqParams.BaseParams.Method + " request",
   362  			Status:  resp.StatusCode,
   363  			Method:  reqParams.BaseParams.Method,
   364  			URLPath: reqParams.Path,
   365  		}
   366  	}
   367  
   368  	herr := &cmn.ErrHTTP{}
   369  	if err := jsoniter.Unmarshal(b, herr); err == nil {
   370  		return herr
   371  	}
   372  	// otherwise, recreate
   373  	msg := string(b)
   374  	return &cmn.ErrHTTP{
   375  		TypeCode: cmn.TypeCodeHTTPErr(msg),
   376  		Message:  msg,
   377  		Status:   resp.StatusCode,
   378  		Method:   reqParams.BaseParams.Method,
   379  		URLPath:  reqParams.Path,
   380  	}
   381  }
   382  
   383  /////////////
   384  // reqResp //
   385  /////////////
   386  
   387  func (rr *reqResp) call() (status int, err error) {
   388  	rr.resp, err = rr.client.Do(rr.req) //nolint:bodyclose // closed by a caller
   389  	if rr.resp != nil {
   390  		status = rr.resp.StatusCode
   391  	}
   392  	return
   393  }
   394  
   395  //
   396  // mem-pools
   397  //
   398  
   399  var (
   400  	reqParamPool sync.Pool
   401  	reqParams0   ReqParams
   402  
   403  	msgpPool sync.Pool
   404  )
   405  
   406  func AllocRp() *ReqParams {
   407  	if v := reqParamPool.Get(); v != nil {
   408  		return v.(*ReqParams)
   409  	}
   410  	return &ReqParams{}
   411  }
   412  
   413  func FreeRp(reqParams *ReqParams) {
   414  	*reqParams = reqParams0
   415  	reqParamPool.Put(reqParams)
   416  }
   417  
   418  func allocMbuf() (buf []byte) {
   419  	if v := msgpPool.Get(); v != nil {
   420  		buf = *(v.(*[]byte))
   421  	} else {
   422  		buf = make([]byte, msgpBufSize)
   423  	}
   424  	return
   425  }
   426  
   427  func freeMbuf(buf []byte) { msgpPool.Put(&buf) }