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, 02:15 UTC
reverseproxy: honor health check redirect destinations (#8223)
Hitanshu committed
Oct 16, 2026 at 17:54 UTC
1f099b3b860f20945567a90b433209fa51c88780
2 files changed
+105
modules/caddyhttp/reverseproxy/active_health_test.go
+98
@@ -218,3 +218,101 @@ func TestActiveHealthCheckBodyStillReplacesKnownPlaceholders(t *testing.T) {
218
t.Errorf("health check body = %q, want %q", got, want)
219
}
220
}
221
+
222
+func TestActiveHealthCheckRedirectDialAddress(t *testing.T) {
223
+ for _, tc := range []struct {
224
+ name string
225
+ follow bool
226
+ relative bool
227
+ returnToOrigin bool
228
+ tls bool
229
+ targetStatus int
230
+ wantHealthy bool
231
+ wantTargetHits int32
232
+ }{
233
+ {name: "redirects disabled", targetStatus: 200},
234
+ {name: "relative redirect preserves dial override", follow: true, relative: true, targetStatus: 200, wantHealthy: true},
235
+ {name: "different port healthy", follow: true, targetStatus: 200, wantHealthy: true, wantTargetHits: 1},
236
+ {name: "different port unhealthy", follow: true, targetStatus: 503, wantTargetHits: 1},
237
+ {name: "HTTPS redirect", follow: true, tls: true, targetStatus: 200, wantHealthy: true, wantTargetHits: 1},
238
+ {name: "return to original dial override", follow: true, returnToOrigin: true, targetStatus: 200, wantHealthy: true, wantTargetHits: 1},
239
+ } {
240
+ t.Run(tc.name, func(t *testing.T) {
241
+ var targetHits, wrongOriginHits atomic.Int32
242
+ targetHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
243
+ targetHits.Add(1)
244
+ if tc.returnToOrigin {
245
+ http.Redirect(w, r, "http://health.invalid/ready", http.StatusFound)
246
+ return
247
+ }
248
+ w.WriteHeader(tc.targetStatus)
249
+ })
250
+ var target *httptest.Server
251
+ if tc.tls {
252
+ target = httptest.NewTLSServer(targetHandler)
253
+ } else {
254
+ target = httptest.NewServer(targetHandler)
255
+ }
256
+ defer target.Close()
257
+
258
+ origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
259
+ if r.URL.Path == "/health" {
260
+ location := target.URL + "/ready"
261
+ if tc.relative {
262
+ location = "/ready"
263
+ }
264
+ http.Redirect(w, r, location, http.StatusFound)
265
+ return
266
+ }
267
+ if tc.relative || tc.returnToOrigin {
268
+ w.WriteHeader(tc.targetStatus)
269
+ return
270
+ }
271
+ wrongOriginHits.Add(1)
272
+ if tc.targetStatus == http.StatusOK {
273
+ w.WriteHeader(http.StatusServiceUnavailable)
274
+ } else {
275
+ w.WriteHeader(http.StatusOK)
276
+ }
277
+ }))
278
+ defer origin.Close()
279
+ addr := strings.TrimPrefix(origin.URL, "http://")
280
+ h, u, cancel := newActiveHandler(t, addr, "/health", 1)
281
+ defer cancel()
282
+ defer hosts.Delete(addr)
283
+
284
+ transport := new(HTTPTransport)
285
+ if err := transport.Provision(h.ctx); err != nil {
286
+ t.Fatalf("provisioning HTTP transport: %v", err)
287
+ }
288
+ defer transport.Cleanup()
289
+ transport.Transport.Proxy = nil
290
+ if tc.tls {
291
+ transport.Transport.TLSClientConfig = target.Client().Transport.(*http.Transport).TLSClientConfig.Clone()
292
+ }
293
+ h.HealthChecks.Active.httpClient.Transport = transport
294
+ h.HealthChecks.Active.FollowRedirects = tc.follow
295
+
296
+ dialInfo, err := u.fillDialInfo(caddy.NewReplacer())
297
+ if err != nil {
298
+ t.Fatalf("filling dial info: %v", err)
299
+ }
300
+ hostAddr := addr
301
+ if tc.relative || tc.returnToOrigin {
302
+ hostAddr = "health.invalid"
303
+ }
304
+ if err := h.doActiveHealthCheck(dialInfo, hostAddr, addr, u); err != nil {
305
+ t.Fatalf("active health check: %v", err)
306
+ }
307
+ if got := u.Healthy(); got != tc.wantHealthy {
308
+ t.Errorf("healthy = %v, want %v", got, tc.wantHealthy)
309
+ }
310
+ if got := targetHits.Load(); got != tc.wantTargetHits {
311
+ t.Errorf("redirect target requests = %d, want %d", got, tc.wantTargetHits)
312
+ }
313
+ if got := wrongOriginHits.Load(); got != 0 {
314
+ t.Errorf("redirect requests sent to original backend = %d, want 0", got)
315
+ }
316
+ })
317
+ }
318
+}
modules/caddyhttp/reverseproxy/healthchecks.go
+7
@@ -181,6 +181,13 @@ func (a *ActiveHealthChecks) Provision(ctx caddy.Context, h *Handler) error {
181
if !a.FollowRedirects {
182
return http.ErrUseLastResponse
183
}
184
+ if req.URL.Scheme != via[0].URL.Scheme || !strings.EqualFold(req.URL.Host, via[0].URL.Host) {
185
+ // Mask the initial upstream's dial override so the transport
186
+ // connects to the redirect URL instead. Compare with the first
187
+ // request so redirects back retain the original override.
188
+ ctx := context.WithValue(req.Context(), dialInfoCtxKey, struct{}{})
189
+ *req = *req.WithContext(ctx)
190
+ }
191
return nil
192
},
193
}