httpretrier

httpretrier wraps an existing HTTP client with bounded retries, choosing the policy from the request method so reads and writes are treated differently.

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

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

Adds retries to an HTTP client you already have.

r, err := httpretrier.New(client, httpretrier.WithAttempts(3))

resp, err := r.Do(req)

It wraps, rather than replaces

Retries are applied per request, around an existing HTTPClient, so a client that is already instrumented, authenticated, or both keeps everything it had.

The alternative shape, a retrying client that you construct instead of yours, forces a choice between retries and whatever the other client was doing. Here httpclient provides tracing, logging and redaction, and this wraps it.

The retry decision sees both the response and the error

RetryIfFn receives *http.Response and error:

func(resp *http.Response, err error) bool

Both are needed. A transport failure gives an error and no response. A 503 gives a response and no error, and is often the more retryable of the two. A policy that only inspects the error misses every server-side failure that completed the round trip.

The default retries only when err != nil, which is the conservative starting point.

Read and write policies are not the same

Three built-in policies:

PolicyFor
RetryIfForReadRequestsidempotent reads, GET and similar
RetryIfForWriteRequestsstate-changing writes, POST, PUT, PATCH
RetryIfFnByHTTPMethodpicks one of the above from the method

The distinction exists because a failed write is genuinely ambiguous. When a POST times out, the request may have been received and processed, with only the response lost. Retrying it can duplicate the effect: a second charge, a second order, a second message.

So the write policy is narrower. It retries the cases where the request demonstrably did not reach the server, and not the cases where it might have.

RetryIfFnByHTTPMethod is the sane default for a general-purpose client, since it applies the right policy without the call site having to choose. For a non-idempotent write, an idempotency key is what makes retrying safe, whatever the policy says; stringkey generates a stable one.

Backoff

The delay schedule comes from backoff, configured through WithAttempts, WithDelay, WithDelayFactor, WithJitter, WithMaxDelay and WithJitterStrategy.

Sharing the schedule with retrier means one retry policy applies consistently to HTTP and non-HTTP work, rather than two implementations drifting apart.

Set WithMaxDelay. With a factor of 2 and no ceiling, attempt counts above a handful reach delays measured in minutes.

Retries multiply load on a struggling service

Worth stating plainly: three attempts per request means a degraded upstream receives three times the traffic at exactly the moment it is least able to handle it. Jitter spreads the arrivals but does not reduce the total.

Keep the attempt count low, keep the budget inside the caller’s deadline, and count retries so the pattern is visible before it becomes an outage.

Dependencies

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