relay.

Examples

Patterns taken from typical service-to-service use. Each snippet is self-contained and uses only the standard library plus relay.

JSON GET with decoding

Fetch and decode a JSON document, retrying transient upstream failures.

type Account struct {
	ID      string `json:"id"`
	Balance int64  `json:"balance_cents"`
}

func (s *Service) Account(ctx context.Context, id string) (*Account, error) {
	resp, err := s.client.Get(ctx, "http://billing.internal/v1/accounts/"+id)
	if err != nil {
		return nil, fmt.Errorf("fetch account: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("unexpected status %d", resp.StatusCode)
	}

	var acct Account
	if err := json.NewDecoder(resp.Body).Decode(&acct); err != nil {
		return nil, fmt.Errorf("decode account: %w", err)
	}
	return &acct, nil
}

Idempotent POST with a retryable body

Because the body comes from a bytes.Reader, relay can rewind and retry it. Pair it with a classifier that retries on 5xx.

client := relay.New(
	relay.WithTimeout(1500*time.Millisecond),
	relay.WithRetry(relay.RetryPolicy{
		MaxAttempts: 3,
		RetryOn: func(resp *http.Response, err error) bool {
			return err != nil || resp.StatusCode >= 500
		},
	}),
)

payload := []byte(`{"event":"ping"}`)
resp, err := client.Post(ctx,
	"http://events.internal/v1/ingest",
	"application/json",
	bytes.NewReader(payload),
)
if err != nil {
	return err
}
defer resp.Body.Close()

Honouring Retry-After on 429

A custom classifier paired with a constant backoff respects a server that asks clients to slow down.

client := relay.New(
	relay.WithRetry(relay.RetryPolicy{
		MaxAttempts: 5,
		RetryOn: func(resp *http.Response, err error) bool {
			if err != nil {
				return true
			}
			return resp.StatusCode == http.StatusTooManyRequests
		},
	}),
	relay.WithBackoff(relay.ConstantBackoff{Delay: time.Second}),
)
Note: relay does not parse Retry-After for you. If you need exact header-driven delays, implement the Backoff interface and read the header from a value stashed in the context.

Per-upstream breaker with a fallback

When the breaker is open, serve cached or default data instead of failing the caller.

client := relay.New(
	relay.WithTimeout(800*time.Millisecond),
	relay.WithCircuitBreaker(relay.BreakerConfig{
		FailureThreshold: 5,
		SuccessThreshold: 2,
		OpenTimeout:      15 * time.Second,
	}),
)

func (s *Service) Recommendations(ctx context.Context, user string) ([]Item, error) {
	resp, err := s.client.Get(ctx, "http://recs.internal/v1/for/"+user)
	if errors.Is(err, relay.ErrCircuitOpen) {
		return s.defaultRecommendations(), nil // graceful degradation
	}
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()
	return decodeItems(resp.Body)
}

Sharing a tuned transport

Give relay an explicit transport when you need connection-pool control.

tr := &http.Transport{
	MaxIdleConns:        100,
	MaxIdleConnsPerHost: 16,
	IdleConnTimeout:     90 * time.Second,
	ForceAttemptHTTP2:   true,
}

client := relay.New(
	relay.WithTransport(tr),
	relay.WithTimeout(2*time.Second),
	relay.WithRetry(relay.RetryPolicy{MaxAttempts: 3, RetryOn: relay.RetryIdempotent}),
)

Exposing breaker state on a health endpoint

func (s *Service) handleHealth(w http.ResponseWriter, r *http.Request) {
	state := s.client.State()
	if state == relay.StateOpen {
		w.WriteHeader(http.StatusServiceUnavailable)
	}
	fmt.Fprintf(w, "upstream breaker: %s\n", state)
}

Need a parameter explained? See Configuration or the API reference.