exponential/helpers/http/http.go (48 lines of code) (raw):
/*
Package http provides an ErrTransformer for http.Client from the standard library.
Other third-party HTTP clients are not supported by this package.
Example that handle HTTP non-temporary error codes:
httpTransform := http.New()
backoff := exponential.WithErrTransformer(httpTransform)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
var resp *http.Response
err := backoff.Retry(
ctx,
func(ctx context.Context, r Record) error {
var err error
resp, err = httpClient.Do(someRequest)
return err
},
)
cancel()
Example with custom errors:
bodyHasErr := func(r *http.Response) error {
b, err :io.ReadAll(r.Body)
if err != nil {
return fmt.Errorf("response body had error: %s", err)
}
s := strings.TrimSpace(string(b))
if strings.HasPrefix(s, "error") {
if strings.Contains(s, "errors: permament") {
return fmt.Errorf("error: %w: %w", s, errors.ErrPermanent)
}
return fmt.Errorf("error: %s", s)
}
return nil
}
httpTransform := http.New(bodyHasErr)
backoff := exponential.WithErrTransformer(httpTransform)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
var resp *http.Response
err := backoff.Retry(
ctx,
func(ctx context.Context, r Record) error {
var err error
resp, err = httpTransform.RespToErr(httpClient.Do(someRequest)) // <- note the call wrapper
return err
},
)
cancel()
*/
package http
import (
"fmt"
"net/http"
"net/url"
"github.com/Azure/retry/internal/errors"
)
// Transformer provides an ErrTransformer method that can be used to detect non-retriable errors.
// The following codes are retriable: StatusRequestTimeout, StatusConflict, StatusLocked, StatusTooEarly,
// StatusTooManyRequests, StatusInternalServerError and StatusGatewayTimeout.
// Any other code is not.
type Transformer struct {
respToErrs []RespToErr
}
// RespToErr allows you to inspect a Response and determine if the result is really an error.
// If you want to make that type of error non-retriable, wrap the error with errors.ErrPermanent, like
// so: return fmt.Errorf("had some error condition: %w", errors.ErrPermanent) . This should return
// nil if the Response was fine.
type RespToErr func(r *http.Response) error
// New returns a new Transformer. This implements exponential.ErrTransformer with the method ErrTransformer.
func New(respToErrs ...RespToErr) *Transformer {
return &Transformer{respToErrs: respToErrs}
}
// ErrTransformer returns a transformer that can be used to detect non-retriable errors.
// If the error is of type *url.Error (the type returned by http.Client) and is .Temporary() == false,
// this will mark the error as a permanent error. Otherwise it will return the error.
// If it is non-retriable it will wrap the error with errors.ErrPermanent. This is meant to be used
// with .RespToErr() which will return an error based on the content of a http.Response.
func (t *Transformer) ErrTransformer(err error) error {
switch e := err.(type) {
case *url.Error:
if !e.Temporary() {
return fmt.Errorf("%w: %w", err, errors.ErrPermanent)
}
return err
}
return err
}
// RespToErr takes an http.Resp and an error from an http.Client call method and returns the Response
// and an error. If error != nil , this simply return the values passed. Otherwise it will inspect the
// Response accord to rules passed to New() to determine if we have an error. It will always execute
// all error RespToErr(s) unless the error returned is wrapped with ErrPermanent.
func (t *Transformer) RespToErr(r *http.Response, err error) (*http.Response, error) {
if len(t.respToErrs) == 0 {
return r, err
}
if err != nil {
return r, err
}
var retErr error
for _, respToErr := range t.respToErrs {
wasPermanent := false
if err = respToErr(r); err != nil {
wasPermanent = errors.Is(err, errors.ErrPermanent)
if retErr == nil {
retErr = err
} else {
retErr = fmt.Errorf("%w: %w", retErr, err)
}
if wasPermanent {
break
}
}
}
return r, retErr
}