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