Repository: labstack/echo
File: middleware/proxy.go (Go)

Code before:
	return b.targets[b.random.Intn(len(b.targets))]
}

// Next returns an upstream target using round-robin technique.
//
// Note: `nil` is returned in case upstream target list is empty.
func (b *roundRobinBalancer) Next(c echo.Context) *ProxyTarget {
	b.mutex.Lock()
	defer b.mutex.Unlock()
	if len(b.targets) == 0 {
		return nil
	} else if len(b.targets) == 1 {
		return b.targets[0]
	}

	var i int
	const lastIdxKey = "_round_robin_last_index"
	// This request is a retry, start from the index of the previous
	// target to ensure we don't attempt to retry the request with
	// the same failed target
	if c.Get(lastIdxKey) != nil {
		i = c.Get(lastIdxKey).(int)
		i++
		if i >= len(b.targets) {
			i = 0
		}
		// This is a first time request, use the global index
	} else {
		i = b.i
		b.i++
		if b.i >= len(b.targets) {
			b.i = 0
		}
	}

	c.Set(lastIdxKey, i)
	return b.targets[i]
}

// Proxy returns a Proxy middleware.
//
// Proxy middleware forwards the request to upstream server using a configured load balancing technique.
func Proxy(balancer ProxyBalancer) echo.MiddlewareFunc {
	c := DefaultProxyConfig
	c.Balancer = balancer
	return ProxyWithConfig(c)
}

// ProxyWithConfig returns a Proxy middleware with config.
// See: `Proxy()`
func ProxyWithConfig(config ProxyConfig) echo.MiddlewareFunc {

Review comment (suggestion):
I do not think this is bullet proof. Targets list can be mutated between retries so that index does could point to different target potentially. 

I suggest:

1. add `contextKey string` field to `roundRobinBalancer` struct
2. add method for setting it go-routine safe way
```go

func (b *roundRobinBalancer) SetContextKey(contextKey string) {
	b.mutex.Lock()
	defer b.mutex.Unlock()

	b.contextKey = contextKey
}
```
3. In `ProxyWithConfig` setup part lets set that key when balancer is of type `roundRobinBalancer`
```go
	if b, ok := config.Balancer.(*roundRobinBalancer); ok {
		b.SetContextKey(config.ContextKey)
	}
```
4. and in `roundRobinBalancer.Next` get it from context and choose next target that is not that old one

Code after:
	return b.targets[b.random.Intn(len(b.targets))]
}

// Next returns an upstream target using round-robin technique. In the case
// where a previously failed request is being retried, the round-robin
// balancer will attempt to use the next target relative to the original
// request. If the list of targets held by the balancer is modified while a
// failed request is being retried, it is possible that the balancer will
// return the original failed target.
//
// Note: `nil` is returned in case upstream target list is empty.
func (b *roundRobinBalancer) Next(c echo.Context) *ProxyTarget {
	b.mutex.Lock()
	defer b.mutex.Unlock()
	if len(b.targets) == 0 {
		return nil
	} else if len(b.targets) == 1 {
		return b.targets[0]
	}

	var i int
	const lastIdxKey = "_round_robin_last_index"
	// This request is a retry, start from the index of the previous
	// target to ensure we don't attempt to retry the request with
	// the same failed target
	if c.Get(lastIdxKey) != nil {
		i = c.Get(lastIdxKey).(int)
		i++
		if i >= len(b.targets) {
			i = 0
		}
	} else {
		// This is a first time request, use the global index
		if b.i >= len(b.targets) {
			b.i = 0
		}
		i = b.i
		b.i++
	}

	c.Set(lastIdxKey, i)
	return b.targets[i]
}

// Proxy returns a Proxy middleware.
//
// Proxy middleware forwards the request to upstream server using a configured load balancing technique.
func Proxy(balancer ProxyBalancer) echo.MiddlewareFunc {
	c := DefaultProxyConfig
	c.Balancer = balancer
	return ProxyWithConfig(c)