send.go raw

   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