go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/api/internal/gensupport/send.go (about) 1 // Copyright 2016 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package gensupport 6 7 import ( 8 "context" 9 "encoding/json" 10 "errors" 11 "fmt" 12 "net/http" 13 "strings" 14 "time" 15 16 "github.com/google/uuid" 17 "github.com/googleapis/gax-go/v2" 18 "github.com/googleapis/gax-go/v2/callctx" 19 ) 20 21 // Use this error type to return an error which allows introspection of both 22 // the context error and the error from the service. 23 type wrappedCallErr struct { 24 ctxErr error 25 wrappedErr error 26 } 27 28 func (e wrappedCallErr) Error() string { 29 return fmt.Sprintf("retry failed with %v; last error: %v", e.ctxErr, e.wrappedErr) 30 } 31 32 func (e wrappedCallErr) Unwrap() error { 33 return e.wrappedErr 34 } 35 36 // Is allows errors.Is to match the error from the call as well as context 37 // sentinel errors. 38 func (e wrappedCallErr) Is(target error) bool { 39 return errors.Is(e.ctxErr, target) || errors.Is(e.wrappedErr, target) 40 } 41 42 // SendRequest sends a single HTTP request using the given client. 43 // If ctx is non-nil, it calls all hooks, then sends the request with 44 // req.WithContext, then calls any functions returned by the hooks in 45 // reverse order. 46 func SendRequest(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) { 47 // Add headers set in context metadata. 48 if ctx != nil { 49 headers := callctx.HeadersFromContext(ctx) 50 for k, vals := range headers { 51 for _, v := range vals { 52 req.Header.Add(k, v) 53 } 54 } 55 } 56 57 // Disallow Accept-Encoding because it interferes with the automatic gzip handling 58 // done by the default http.Transport. See https://github.com/google/google-api-go-client/issues/219. 59 if _, ok := req.Header["Accept-Encoding"]; ok { 60 return nil, errors.New("google api: custom Accept-Encoding headers not allowed") 61 } 62 if ctx == nil { 63 return client.Do(req) 64 } 65 return send(ctx, client, req) 66 } 67 68 func send(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) { 69 if client == nil { 70 client = http.DefaultClient 71 } 72 resp, err := client.Do(req.WithContext(ctx)) 73 // If we got an error, and the context has been canceled, 74 // the context's error is probably more useful. 75 if err != nil { 76 select { 77 case <-ctx.Done(): 78 err = ctx.Err() 79 default: 80 } 81 } 82 return resp, err 83 } 84 85 // SendRequestWithRetry sends a single HTTP request using the given client, 86 // with retries if a retryable error is returned. 87 // If ctx is non-nil, it calls all hooks, then sends the request with 88 // req.WithContext, then calls any functions returned by the hooks in 89 // reverse order. 90 func SendRequestWithRetry(ctx context.Context, client *http.Client, req *http.Request, retry *RetryConfig) (*http.Response, error) { 91 // Add headers set in context metadata. 92 if ctx != nil { 93 headers := callctx.HeadersFromContext(ctx) 94 for k, vals := range headers { 95 for _, v := range vals { 96 req.Header.Add(k, v) 97 } 98 } 99 } 100 101 // Disallow Accept-Encoding because it interferes with the automatic gzip handling 102 // done by the default http.Transport. See https://github.com/google/google-api-go-client/issues/219. 103 if _, ok := req.Header["Accept-Encoding"]; ok { 104 return nil, errors.New("google api: custom Accept-Encoding headers not allowed") 105 } 106 if ctx == nil { 107 return client.Do(req) 108 } 109 return sendAndRetry(ctx, client, req, retry) 110 } 111 112 func sendAndRetry(ctx context.Context, client *http.Client, req *http.Request, retry *RetryConfig) (*http.Response, error) { 113 if client == nil { 114 client = http.DefaultClient 115 } 116 117 var resp *http.Response 118 var err error 119 attempts := 1 120 invocationID := uuid.New().String() 121 baseXGoogHeader := req.Header.Get("X-Goog-Api-Client") 122 123 // Loop to retry the request, up to the context deadline. 124 var pause time.Duration 125 var bo Backoff 126 if retry != nil && retry.Backoff != nil { 127 bo = &gax.Backoff{ 128 Initial: retry.Backoff.Initial, 129 Max: retry.Backoff.Max, 130 Multiplier: retry.Backoff.Multiplier, 131 } 132 } else { 133 bo = backoff() 134 } 135 136 var errorFunc = retry.errorFunc() 137 138 for { 139 t := time.NewTimer(pause) 140 select { 141 case <-ctx.Done(): 142 t.Stop() 143 // If we got an error and the context has been canceled, return an error acknowledging 144 // both the context cancelation and the service error. 145 if err != nil { 146 return resp, wrappedCallErr{ctx.Err(), err} 147 } 148 return resp, ctx.Err() 149 case <-t.C: 150 } 151 152 if ctx.Err() != nil { 153 // Check for context cancellation once more. If more than one case in a 154 // select is satisfied at the same time, Go will choose one arbitrarily. 155 // That can cause an operation to go through even if the context was 156 // canceled before. 157 if err != nil { 158 return resp, wrappedCallErr{ctx.Err(), err} 159 } 160 return resp, ctx.Err() 161 } 162 163 // Set retry metrics and idempotency headers for GCS. 164 // TODO(b/274504690): Consider dropping gccl-invocation-id key since it 165 // duplicates the X-Goog-Gcs-Idempotency-Token header (added in v0.115.0). 166 invocationHeader := fmt.Sprintf("gccl-invocation-id/%s gccl-attempt-count/%d", invocationID, attempts) 167 xGoogHeader := strings.Join([]string{invocationHeader, baseXGoogHeader}, " ") 168 req.Header.Set("X-Goog-Api-Client", xGoogHeader) 169 req.Header.Set("X-Goog-Gcs-Idempotency-Token", invocationID) 170 171 resp, err = client.Do(req.WithContext(ctx)) 172 173 var status int 174 if resp != nil { 175 status = resp.StatusCode 176 } 177 178 // Check if we can retry the request. A retry can only be done if the error 179 // is retryable and the request body can be re-created using GetBody (this 180 // will not be possible if the body was unbuffered). 181 if req.GetBody == nil || !errorFunc(status, err) { 182 break 183 } 184 attempts++ 185 var errBody error 186 req.Body, errBody = req.GetBody() 187 if errBody != nil { 188 break 189 } 190 191 pause = bo.Pause() 192 if resp != nil && resp.Body != nil { 193 resp.Body.Close() 194 } 195 } 196 return resp, err 197 } 198 199 // DecodeResponse decodes the body of res into target. If there is no body, 200 // target is unchanged. 201 func DecodeResponse(target interface{}, res *http.Response) error { 202 if res.StatusCode == http.StatusNoContent { 203 return nil 204 } 205 return json.NewDecoder(res.Body).Decode(target) 206 }