If an http.Client is already shared by concurrent requests, keep its Transport field stable. A wrapper can synchronize which fully configured transport each later round trip receives:

type routeSlot struct {
    mu sync.RWMutex
    current http.RoundTripper
}

func (s *routeSlot) RoundTrip(r *http.Request) (*http.Response, error) {
    s.mu.RLock()
    current := s.current
    s.mu.RUnlock()
    return current.RoundTrip(r)
}

func (s *routeSlot) Set(next http.RoundTripper) {
    s.mu.Lock()
    s.current = next
    s.mu.Unlock()
}

Using net/http and sync, initialize it before publishing the client:

slot := &routeSlot{current: initialTransport}
client := &http.Client{Transport: slot}

slot.Set(nextTransport)

initialTransport and nextTransport must be non-nil, fully configured, concurrency-safe round trippers. Build each proxy transport before publishing it, and leave its configuration unchanged while requests use it.

The lock protects only the pointer snapshot, so a slow request does not hold the routing lock for its entire network exchange. A request already using the previous snapshot continues on that transport; the switch does not replay it or move its connection.

Retain and retire old transports through the application’s bounded lifecycle, allowing in-flight work to finish and closing idle pools when appropriate. A redirect can cause another RoundTrip after a switch. If an entire workflow requires one route, assign that route at the workflow boundary instead of changing this shared selector midway through it.

Erthyglow kevrennek