...

Source file src/google.golang.org/api/internal/gensupport/send.go

Documentation: google.golang.org/api/internal/gensupport

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

View as plain text