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{