diff --git a/pkg/server/service/service.go b/pkg/server/service/service.go index 109a2ed52f..b06ee505c5 100644 --- a/pkg/server/service/service.go +++ b/pkg/server/service/service.go @@ -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) diff --git a/pkg/server/service/service_test.go b/pkg/server/service/service_test.go index 574dd1a5bd..07344b612b 100644 --- a/pkg/server/service/service_test.go +++ b/pkg/server/service/service_test.go @@ -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) {}