Preserve health check status updater when service has middlewares

This commit is contained in:
Harold Ozouf 2026-03-19 14:16:07 +01:00 committed by GitHub
parent 86db5c2777
commit 6c7c056b28
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 88 additions and 0 deletions

View File

@ -189,12 +189,16 @@ func (m *Manager) BuildHTTP(rootCtx context.Context, serviceName string) (http.H
return nil, errors.New("chain builder not defined")
}
chain := m.middlewareChainBuilder.BuildMiddlewareChain(ctx, conf.Middlewares)
originalLB := lb
var err error
lb, err = chain.Then(lb)
if err != nil {
conf.AddError(err, true)
return nil, err
}
if su, ok := originalLB.(healthcheck.StatusUpdater); ok {
lb = &statusUpdaterHandler{Handler: lb, statusUpdater: su}
}
}
m.services[serviceName] = lb
@ -537,6 +541,18 @@ type serverBalancer interface {
AddServer(name string, handler http.Handler, server dynamic.Server)
}
// statusUpdaterHandler wraps an http.Handler while preserving the
// healthcheck.StatusUpdater interface from the original handler.
type statusUpdaterHandler struct {
http.Handler
statusUpdater healthcheck.StatusUpdater
}
func (s *statusUpdaterHandler) RegisterStatusUpdater(fn func(up bool)) error {
return s.statusUpdater.RegisterStatusUpdater(fn)
}
func shuffle[T any](values []T, r *rand.Rand) []T {
shuffled := make([]T, len(values))
copy(shuffled, values)

View File

@ -12,6 +12,7 @@ import (
"testing"
"time"
"github.com/containous/alice"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
ptypes "github.com/traefik/paerser/types"
@ -729,6 +730,77 @@ func (s serviceBuilderFunc) BuildHTTP(ctx context.Context, serviceName string) (
return s(ctx, serviceName)
}
func TestGetServiceHandler_HealthCheck(t *testing.T) {
pb := httputil.NewProxyBuilder(&transportManagerMock{}, nil)
testCases := []struct {
desc string
withMiddleware bool
}{
{
desc: "without service middleware",
},
{
desc: "with service middleware",
withMiddleware: true,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
t.Cleanup(backend.Close)
childSvc := &dynamic.Service{
LoadBalancer: &dynamic.ServersLoadBalancer{
Servers: []dynamic.Server{{URL: backend.URL}},
HealthCheck: &dynamic.ServerHealthCheck{
Path: "/health",
},
},
}
if test.withMiddleware {
childSvc.Middlewares = []string{"add-header@file"}
}
configs := map[string]*runtime.ServiceInfo{
"child@file": {Service: childSvc},
"wrr@file": {
Service: &dynamic.Service{
Weighted: &dynamic.WeightedRoundRobin{
Services: []dynamic.WRRService{{Name: "child@file", Weight: pointer(1)}},
HealthCheck: &dynamic.HealthCheck{},
},
},
},
}
manager := NewManager(configs, nil, nil, &transportManagerMock{}, pb)
if test.withMiddleware {
manager.SetMiddlewareChainBuilder(&noopMiddlewareChainBuilder{})
}
_, err := manager.BuildHTTP(t.Context(), "wrr@file")
require.NoError(t, err)
})
}
}
// noopMiddlewareChainBuilder wraps a handler in a plain http.HandlerFunc,
// simulating the effect of service-level middlewares without needing real middleware config.
type noopMiddlewareChainBuilder struct{}
func (n *noopMiddlewareChainBuilder) BuildMiddlewareChain(_ context.Context, _ []string) *alice.Chain {
chain := alice.New(func(next http.Handler) (http.Handler, error) {
return http.HandlerFunc(next.ServeHTTP), nil
})
return &chain
}
type internalHandler struct{}
func (internalHandler) ServeHTTP(_ http.ResponseWriter, _ *http.Request) {}