go 37 lines · 1 tab

HTTP client middleware via RoundTripper (headers + timing)

Leah Thompson Jan 2026
1 tab
package deps

import (
  "net/http"
  "time"

  "go.uber.org/zap"
)

type Transport struct {
  Base http.RoundTripper
  Log  *zap.Logger
  ReqID func() string
}

func (t Transport) RoundTrip(req *http.Request) (*http.Response, error) {
  base := t.Base
  if base == nil {
    base = http.DefaultTransport
  }

  if t.ReqID != nil {
    req.Header.Set("X-Request-Id", t.ReqID())
  }

  start := time.Now()
  resp, err := base.RoundTrip(req)
  if t.Log != nil {
    t.Log.Info("http.client",
      zap.String("method", req.Method),
      zap.String("host", req.URL.Host),
      zap.Duration("duration", time.Since(start)),
      zap.Error(err),
    )
  }
  return resp, err
}
1 file · go Explain with highlit

When multiple services call the same upstream, I like a custom http.RoundTripper to centralize cross-cutting behavior: inject headers, measure duration, and apply consistent redaction rules. This keeps call sites clean and prevents copy/paste mistakes like forgetting to set User-Agent or missing an auth header for a new endpoint. The wrapper below adds an X-Request-Id and records a simple log line with duration; in real systems I’d record a Prometheus histogram and emit trace spans. The important design detail is to preserve the underlying transport so connection pooling still works; you wrap Transport, you don’t replace it with ad-hoc http.Clients. This pattern scales nicely as you add retries, rate limiting, and circuit breakers.


Related snips

Share this code

Here's the card — post it anywhere.

HTTP client middleware via RoundTripper (headers + timing) — share card
Link copied