@marcel91200 / caddy Mirror · read-only / commits / 55b3397a
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 09:45 UTC

reverseproxy: validate on weighted_round_robin loadbalancing policy (#7807)

* reverseproxy: validate on weighted_round_robin policy Validate that weighted_round_robin has a non-zero total weight. This prevents configurations such as: weighted_round_robin 0 0 from being accepted and causing a divide-by-zero panic during request handling. * test: validation test on zero weight upstreams. * test: provision called instead of totalweight setting * reverseproxy: validate on negative upstream weights * test: regression test on weighted_round_robin selection policy

Rhul committed Jun 7, 2026 at 21:48 UTC 55b3397a2da2af0a8d567b63460af1be8e6820a7
3 files changed +123 -5
caddytest/integration/reverseproxy_test.go
+67 -5
@@ -199,7 +199,7 @@ func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) {
199 ],
200 "handle": [
201 {
202 -
202 +
203 "handler": "reverse_proxy",
204 "upstreams": [
205 {
@@ -293,7 +293,7 @@ func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) {
293 ],
294 "handle": [
295 {
296 -
296 +
297 "handler": "reverse_proxy",
298 "upstreams": [
299 {
@@ -374,7 +374,7 @@ func TestReverseProxyHealthCheck(t *testing.T) {
374 http://localhost:9080 {
375 reverse_proxy {
376 to localhost:2020
377 -
377 +
378 health_uri /health
379 health_port 2021
380 health_interval 10ms
@@ -495,7 +495,7 @@ func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
495 http://localhost:9080 {
496 reverse_proxy {
497 to unix/%s
498 -
498 +
499 health_uri /health
500 health_port 2021
501 health_interval 2s
@@ -553,7 +553,7 @@ func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) {
553 http://localhost:9080 {
554 reverse_proxy {
555 to unix/%s
556 -
556 +
557 health_uri /health
558 health_interval 2s
559 health_timeout 5s
@@ -831,3 +831,65 @@ func TestReverseProxySNIPlaceHolder(t *testing.T) {
831 tester.AssertResponse(req, 200, "example.com")
832 }
833 }
834 +
835 +func TestWeightedRoundRobinSelectionValidation(t *testing.T) {
836 + configTemplate := `
837 + {
838 + "apps": {
839 + "http": {
840 + "servers": {
841 + "srv0": {
842 + "listen": [":18080"],
843 + "routes": [
844 + {
845 + "handle": [
846 + {
847 + "handler": "reverse_proxy",
848 + "load_balancing": {
849 + "selection_policy": {
850 + "policy": "weighted_round_robin",
851 + "weights": %s
852 + }
853 + },
854 + "upstreams": [
855 + {"dial": "localhost:18081"},
856 + {"dial": "localhost:18082"}
857 + ]
858 + }
859 + ]
860 + }
861 + ]
862 + }
863 + }
864 + }
865 + }
866 + }`
867 +
868 + tests := []struct {
869 + name string
870 + weights string
871 + errMsg string
872 + }{
873 + {
874 + name: "negative weight",
875 + weights: "[-1, 2]",
876 + errMsg: "weight of an upstream cannot be negative",
877 + },
878 + {
879 + name: "zero total weight",
880 + weights: "[0, 0]",
881 + errMsg: "requires at least one upstream with a positive weight",
882 + },
883 + }
884 +
885 + for _, tc := range tests {
886 + t.Run(tc.name, func(t *testing.T) {
887 + caddytest.AssertLoadError(
888 + t,
889 + fmt.Sprintf(configTemplate, tc.weights),
890 + "json",
891 + tc.errMsg,
892 + )
893 + })
894 + }
895 +}
modules/caddyhttp/reverseproxy/selectionpolicies.go
+14
@@ -127,6 +127,19 @@ func (r *WeightedRoundRobinSelection) Provision(ctx caddy.Context) error {
127 return nil
128 }
129
130 +// Validate ensures that r's configuration is valid
131 +func (r *WeightedRoundRobinSelection) Validate() error {
132 + if r.totalWeight <= 0 {
133 + return fmt.Errorf("weighted_round_robin requires at least one upstream with a positive weight")
134 + }
135 + for _, weight := range r.Weights {
136 + if weight < 0 {
137 + return fmt.Errorf("weight of an upstream cannot be negative")
138 + }
139 + }
140 + return nil
141 +}
142 +
143 // Select returns an available host, if any.
144 func (r *WeightedRoundRobinSelection) Select(pool UpstreamPool, _ *http.Request, _ http.ResponseWriter) *Upstream {
145 if len(pool) == 0 {
@@ -891,6 +904,7 @@ var (
904 _ Selector = (*CookieHashSelection)(nil)
905
906 _ caddy.Validator = (*RandomChoiceSelection)(nil)
907 + _ caddy.Validator = (*WeightedRoundRobinSelection)(nil)
908
909 _ caddy.Provisioner = (*RandomChoiceSelection)(nil)
910 _ caddy.Provisioner = (*WeightedRoundRobinSelection)(nil)
modules/caddyhttp/reverseproxy/selectionpolicies_test.go
+42
@@ -131,6 +131,48 @@ func TestWeightedRoundRobinPolicy(t *testing.T) {
131 }
132 }
133
134 +func TestWeightedRoundRobinSelection_Validate(t *testing.T) {
135 + tests := []struct {
136 + name string
137 + weights []int
138 + wantErr bool
139 + }{
140 + {
141 + name: "Valid 0 2 1 case",
142 + weights: []int{0, 2, 1},
143 + wantErr: false,
144 + },
145 + {
146 + name: "Invalid 0 case (single)",
147 + weights: []int{0},
148 + wantErr: true,
149 + },
150 + {
151 + name: "Invalid 0 0 case (multiple)",
152 + weights: []int{0, 0},
153 + wantErr: true,
154 + },
155 + {
156 + name: "Valid weights",
157 + weights: []int{1, 1, 1},
158 + wantErr: false,
159 + },
160 + }
161 +
162 + for _, tt := range tests {
163 + t.Run(tt.name, func(t *testing.T) {
164 + s := &WeightedRoundRobinSelection{
165 + Weights: tt.weights,
166 + }
167 + _ = s.Provision(caddy.Context{})
168 + err := s.Validate()
169 + if (err != nil) != tt.wantErr {
170 + t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
171 + }
172 + })
173 + }
174 +}
175 +
176 func TestWeightedRoundRobinPolicyWithZeroWeight(t *testing.T) {
177 pool := testPool()
178 wrrPolicy := WeightedRoundRobinSelection{