@marcel91200 / caddy Mirror · read-only / commits / 1f099b3b
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, 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 }