httpretrier

httpretrier provides configurable retry execution for outbound HTTP requests.

Part of nurago, a collection of independent Go packages for backend services.

import "github.com/tecnickcom/nurago/pkg/httpretrier"

Package httpretrier provides configurable retry execution for outbound HTTP requests.

It wraps an HTTPClient with retry orchestration driven by a pluggable RetryIfFn. HTTPRetrier.Do executes a request up to a bounded number of attempts, applying delay growth and jitter between retries. The retry decision function receives both *http.Response and error, enabling policy decisions based on transport failures and/or HTTP status codes.

Retries are applied per request rather than per client, so an already configured client (instrumented, authenticated, or both) keeps its behavior and is wrapped instead of replaced.

Built-in Retry Policies

Predefined helpers are provided for common semantics:

  • RetryIfForReadRequests for idempotent reads (e.g. GET)
  • RetryIfForWriteRequests for state-changing writes (e.g. POST/PUT/PATCH)
  • RetryIfFnByHTTPMethod to select one of the above from method name

The default policy retries only when err != nil.

Backoff Behavior

Delay progression is configurable via:

  • total attempts cap (WithAttempts)
  • initial delay (WithDelay)
  • multiplicative delay factor (WithDelayFactor)
  • random jitter ceiling (WithJitter)
  • maximum delay ceiling (WithMaxDelay)
  • jitter strategy (WithJitterStrategy)

This produces bounded exponential-style backoff with randomization, helping reduce synchronized retry storms. Optionally, WithRespectRetryAfter makes the retrier wait at least the server-provided Retry-After delay, and WithOnRetry exposes each scheduled retry for logging or metrics.

Request Body Replay

When a request has a body and retries are needed, the retrier relies on Request.GetBody to recreate the body stream for subsequent attempts. If the request has a body that cannot be recreated (GetBody missing or failing), retries cannot continue and Do returns an error. Bodyless requests (e.g. a GET with no body) retry without restriction.

When To Use

  • Calls to an upstream API fail intermittently with 429 or 5xx responses.
  • Read and write requests need different retry policies, because replaying a write may duplicate a side effect.
  • The server sends Retry-After and you want it respected, bounded by a cap you choose.

Example

// A server that fails twice before succeeding.
attempts := 0
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
	attempts++

	if attempts < 3 {
		w.WriteHeader(http.StatusServiceUnavailable)

		return
	}

	w.WriteHeader(http.StatusOK)
}))

defer srv.Close()

retrier, err := httpretrier.New(
	srv.Client(),
	httpretrier.WithAttempts(4),
	httpretrier.WithDelay(time.Millisecond),
	httpretrier.WithJitter(time.Microsecond), // kept tiny so the example runs quickly
	httpretrier.WithRetryIfFn(httpretrier.RetryIfForReadRequests),
)
if err != nil {
	fmt.Println(err)

	return
}

req, err := http.NewRequestWithContext(context.TODO(), http.MethodGet, srv.URL, nil)
if err != nil {
	fmt.Println(err)

	return
}

resp, err := retrier.Do(req)
if err != nil {
	fmt.Println(err)

	return
}

defer func() { _ = resp.Body.Close() }()

fmt.Println(resp.StatusCode, "after", attempts, "attempts")

// Output:
// 200 after 3 attempts

Full source is in example_httpretrier_test.go. More runnable examples are on pkg.go.dev.

Dependencies

This package reaches no external module: it uses only the Go standard library.