go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/lhttp/client.go (about)

     1  // Copyright 2015 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package lhttp
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"io"
    21  	"net/http"
    22  
    23  	"go.chromium.org/luci/common/errors"
    24  	"go.chromium.org/luci/common/logging"
    25  	"go.chromium.org/luci/common/retry"
    26  	"go.chromium.org/luci/common/retry/transient"
    27  )
    28  
    29  // Handler is called once or multiple times for each HTTP request that is tried.
    30  type Handler func(*http.Response) error
    31  
    32  // ErrorHandler is called once or multiple times for each HTTP request that is
    33  // tried.  It is called when any non-200 response code is received, or if some
    34  // other network error occurs.
    35  // resp may be nil if a network error occurred before the response was received.
    36  // The ErrorHandler must close the provided resp, if any.
    37  // Return the same error again to continue retry behaviour, or nil to pretend
    38  // this error was a success.
    39  type ErrorHandler func(resp *http.Response, err error) error
    40  
    41  // RequestGen is a generator function to create a new request. It may be called
    42  // multiple times if an operation needs to be retried. The HTTP server is
    43  // responsible for closing the Request body, as per http.Request Body method
    44  // documentation.
    45  type RequestGen func() (*http.Request, error)
    46  
    47  var httpTagKey = errors.NewTagKey("this is an HTTP error")
    48  
    49  func applyHTTPTag(err error, status int) error {
    50  	return errors.TagValue{Key: httpTagKey, Value: status}.Apply(err)
    51  }
    52  
    53  func IsHTTPError(err error) (status int, ok bool) {
    54  	d, ok := errors.TagValueIn(httpTagKey, err)
    55  	if ok {
    56  		status = d.(int)
    57  	}
    58  	return
    59  }
    60  
    61  // NewRequest returns a retriable request.
    62  //
    63  // The handler func is responsible for closing the response Body before
    64  // returning. It should return retry.Error in case of retriable error, for
    65  // example if a TCP connection is terminated while receiving the content.
    66  //
    67  // If rFn is nil, NewRequest will use a default exponential backoff strategy
    68  // only for transient errors.
    69  //
    70  // If errorHandler is nil, the default error handler will drain and close the
    71  // response body.
    72  func NewRequest(ctx context.Context, c *http.Client, rFn retry.Factory, rgen RequestGen,
    73  	handler Handler, errorHandler ErrorHandler) func() (int, error) {
    74  	if rFn == nil {
    75  		rFn = transient.Only(retry.Default)
    76  	}
    77  	if errorHandler == nil {
    78  		errorHandler = func(resp *http.Response, err error) error {
    79  			if resp != nil {
    80  				// Drain and close the resp.Body.
    81  				io.Copy(io.Discard, resp.Body)
    82  				resp.Body.Close()
    83  			}
    84  			return err
    85  		}
    86  	}
    87  
    88  	return func() (int, error) {
    89  		status, attempts := 0, 0
    90  		err := retry.Retry(ctx, rFn, func() error {
    91  			attempts++
    92  			req, err := rgen()
    93  			if err != nil {
    94  				return errors.Annotate(err, "failed to call rgen").Err()
    95  			}
    96  
    97  			resp, err := c.Do(req)
    98  			if err != nil {
    99  				logging.Debugf(ctx, "failed to call c.Do: %v", err)
   100  				err = errors.Annotate(err, "failed to call c.Do").Err()
   101  				// Retry every error. This is sad when you specify an invalid hostname but
   102  				// it's better than failing when DNS resolution is flaky.
   103  				return errorHandler(nil, transient.Tag.Apply(err))
   104  			}
   105  			status = resp.StatusCode
   106  
   107  			switch {
   108  			case status == 408, status == 429, status >= 500:
   109  				// The HTTP status code means the request should be retried.
   110  				err = errors.Reason("http request failed: %s (HTTP %d)", http.StatusText(status), status).
   111  					Tag(transient.Tag).Err()
   112  			case status >= 400:
   113  				// Any other failure code is a hard failure.
   114  				err = fmt.Errorf("http request failed: %s (HTTP %d)", http.StatusText(status), status)
   115  			default:
   116  				// The handler may still return a retry.Error to indicate that the request
   117  				// should be retried even on successful status code.
   118  				err = handler(resp)
   119  				if err != nil {
   120  					return errors.Annotate(err, "failed to handle response").Err()
   121  				}
   122  				return err
   123  			}
   124  
   125  			err = applyHTTPTag(err, status)
   126  			return errorHandler(resp, err)
   127  		}, nil)
   128  		if err != nil {
   129  			err = errors.Annotate(err, "gave up after %d attempts", attempts).Err()
   130  		}
   131  		return status, err
   132  	}
   133  }