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) }