github.com/go-playground/pkg/v5@v5.29.1/net/http/retryable.go (about)

     1  //go:build go1.18
     2  // +build go1.18
     3  
     4  package httpext
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"net/http"
    10  	"strconv"
    11  
    12  	bytesext "github.com/go-playground/pkg/v5/bytes"
    13  	errorsext "github.com/go-playground/pkg/v5/errors"
    14  	. "github.com/go-playground/pkg/v5/values/result"
    15  )
    16  
    17  var (
    18  	// retryableStatusCodes defines the common HTTP response codes that are considered retryable.
    19  	retryableStatusCodes = map[int]bool{
    20  		http.StatusServiceUnavailable: true,
    21  		http.StatusTooManyRequests:    true,
    22  		http.StatusBadGateway:         true,
    23  		http.StatusGatewayTimeout:     true,
    24  		http.StatusRequestTimeout:     true,
    25  
    26  		// 524 is a Cloudflare specific error which indicates it connected to the origin server but did not receive
    27  		// response within 100 seconds and so times out.
    28  		// https://support.cloudflare.com/hc/en-us/articles/115003011431-Error-524-A-timeout-occurred#524error
    29  		524: true,
    30  	}
    31  	// nonRetryableStatusCodes defines common HTTP responses that are not considered never to be retryable.
    32  	nonRetryableStatusCodes = map[int]bool{
    33  		http.StatusBadRequest:                    true,
    34  		http.StatusUnauthorized:                  true,
    35  		http.StatusForbidden:                     true,
    36  		http.StatusNotFound:                      true,
    37  		http.StatusMethodNotAllowed:              true,
    38  		http.StatusNotAcceptable:                 true,
    39  		http.StatusProxyAuthRequired:             true,
    40  		http.StatusConflict:                      true,
    41  		http.StatusLengthRequired:                true,
    42  		http.StatusPreconditionFailed:            true,
    43  		http.StatusRequestEntityTooLarge:         true,
    44  		http.StatusRequestURITooLong:             true,
    45  		http.StatusUnsupportedMediaType:          true,
    46  		http.StatusRequestedRangeNotSatisfiable:  true,
    47  		http.StatusExpectationFailed:             true,
    48  		http.StatusTeapot:                        true,
    49  		http.StatusMisdirectedRequest:            true,
    50  		http.StatusUnprocessableEntity:           true,
    51  		http.StatusPreconditionRequired:          true,
    52  		http.StatusRequestHeaderFieldsTooLarge:   true,
    53  		http.StatusUnavailableForLegalReasons:    true,
    54  		http.StatusNotImplemented:                true,
    55  		http.StatusHTTPVersionNotSupported:       true,
    56  		http.StatusLoopDetected:                  true,
    57  		http.StatusNotExtended:                   true,
    58  		http.StatusNetworkAuthenticationRequired: true,
    59  	}
    60  )
    61  
    62  // ErrRetryableStatusCode can be used to indicate a retryable HTTP status code was encountered as an error.
    63  type ErrRetryableStatusCode struct {
    64  	Response *http.Response
    65  }
    66  
    67  func (e ErrRetryableStatusCode) Error() string {
    68  	return fmt.Sprintf("retryable HTTP status code encountered: %d", e.Response.StatusCode)
    69  }
    70  
    71  // ErrUnexpectedResponse can be used to indicate an unexpected response was encountered as an error and provide access to the *http.Response.
    72  type ErrUnexpectedResponse struct {
    73  	Response *http.Response
    74  }
    75  
    76  func (e ErrUnexpectedResponse) Error() string {
    77  	return "unexpected response encountered"
    78  }
    79  
    80  // IsRetryableStatusCode returns true if the provided status code is considered retryable.
    81  func IsRetryableStatusCode(code int) bool {
    82  	return retryableStatusCodes[code]
    83  }
    84  
    85  // IsNonRetryableStatusCode returns true if the provided status code should generally not be retryable.
    86  func IsNonRetryableStatusCode(code int) bool {
    87  	return nonRetryableStatusCodes[code]
    88  }
    89  
    90  // BuildRequestFn is a function used to rebuild an HTTP request for use in retryable code.
    91  type BuildRequestFn func(ctx context.Context) (*http.Request, error)
    92  
    93  // IsRetryableStatusCodeFn is a function used to determine if the provided status code is considered retryable.
    94  type IsRetryableStatusCodeFn func(code int) bool
    95  
    96  // DoRetryableResponse will execute the provided functions code and automatically retry before returning the *http.Response.
    97  //
    98  // Deprecated: use `httpext.Retrier` instead which corrects design issues with the current implementation.
    99  func DoRetryableResponse(ctx context.Context, onRetryFn errorsext.OnRetryFn[error], isRetryableStatusCode IsRetryableStatusCodeFn, client *http.Client, buildFn BuildRequestFn) Result[*http.Response, error] {
   100  	if client == nil {
   101  		client = http.DefaultClient
   102  	}
   103  	var attempt int
   104  	for {
   105  		req, err := buildFn(ctx)
   106  		if err != nil {
   107  			return Err[*http.Response, error](err)
   108  		}
   109  
   110  		resp, err := client.Do(req)
   111  		if err != nil {
   112  			if retryReason, isRetryable := errorsext.IsRetryableHTTP(err); isRetryable {
   113  				opt := onRetryFn(ctx, err, retryReason, attempt)
   114  				if opt.IsSome() {
   115  					return Err[*http.Response, error](opt.Unwrap())
   116  				}
   117  				attempt++
   118  				continue
   119  			}
   120  			return Err[*http.Response, error](err)
   121  		}
   122  
   123  		if isRetryableStatusCode(resp.StatusCode) {
   124  			opt := onRetryFn(ctx, ErrRetryableStatusCode{Response: resp}, strconv.Itoa(resp.StatusCode), attempt)
   125  			if opt.IsSome() {
   126  				return Err[*http.Response, error](opt.Unwrap())
   127  			}
   128  			attempt++
   129  			continue
   130  		}
   131  		return Ok[*http.Response, error](resp)
   132  	}
   133  }
   134  
   135  // DoRetryable will execute the provided functions code and automatically retry before returning the result.
   136  //
   137  // This function currently supports decoding the following automatically based on the response Content-Type with
   138  // Gzip supported:
   139  // - JSON
   140  // - XML
   141  //
   142  // Deprecated: use `httpext.Retrier` instead which corrects design issues with the current implementation.
   143  func DoRetryable[T any](ctx context.Context, isRetryableFn errorsext.IsRetryableFn[error], onRetryFn errorsext.OnRetryFn[error], isRetryableStatusCode IsRetryableStatusCodeFn, client *http.Client, expectedResponseCode int, maxMemory bytesext.Bytes, buildFn BuildRequestFn) Result[T, error] {
   144  
   145  	return errorsext.DoRetryable(ctx, isRetryableFn, onRetryFn, func(ctx context.Context) Result[T, error] {
   146  
   147  		result := DoRetryableResponse(ctx, onRetryFn, isRetryableStatusCode, client, buildFn)
   148  		if result.IsErr() {
   149  			return Err[T, error](result.Err())
   150  		}
   151  		resp := result.Unwrap()
   152  
   153  		if resp.StatusCode != expectedResponseCode {
   154  			return Err[T, error](ErrUnexpectedResponse{Response: resp})
   155  		}
   156  		defer resp.Body.Close()
   157  
   158  		data, err := DecodeResponse[T](resp, maxMemory)
   159  		if err != nil {
   160  			return Err[T, error](err)
   161  		}
   162  		return Ok[T, error](data)
   163  	})
   164  }