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.