@marcel91200 / caddy Mirror · read-only / commits / 77e9ce74
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, 11:00 UTC

reverseproxy: further prevent body closes from dial errors (#7715)

James Hartig committed May 12, 2026 at 13:05 UTC 77e9ce7404c4a76853e101a9f5687a929ee56654
2 files changed +63 -9
modules/caddyhttp/reverseproxy/retries_test.go
+55
@@ -730,3 +730,58 @@ func TestRetryMatchAllowsExpressionMixedWithOtherMatchers(t *testing.T) {
730 })
731 }
732 }
733 +
734 +// TestSubrouteErrorFallbackWithBody is similar to TestDialErrorBodyRetry but
735 +// mimics Subroute's Error handler rather than testing retries specifically
736 +func TestSubrouteErrorFallbackWithBody(t *testing.T) {
737 + // Good upstream: echoes the request body with 200 OK.
738 + goodServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
739 + body, err := io.ReadAll(r.Body)
740 + if err != nil {
741 + http.Error(w, "read body: "+err.Error(), http.StatusInternalServerError)
742 + return
743 + }
744 + w.WriteHeader(http.StatusOK)
745 + _, err = w.Write(body)
746 + if err != nil {
747 + t.Errorf("error writing in good server: %v", err)
748 + }
749 + }))
750 + t.Cleanup(goodServer.Close)
751 +
752 + // Handler which will dial error
753 + badProxy := minimalHandler(0, &Upstream{Host: new(Host), Dial: deadUpstreamAddr(t)})
754 +
755 + bodyReader := newCloseOnCloseReader("hello world")
756 + req := httptest.NewRequest("POST", "http://localhost/", bodyReader)
757 + // httptest.NewRequest wraps the reader in NopCloser; replace
758 + // it with our close-aware reader so Close() is propagated.
759 + req.Body = bodyReader
760 +
761 + req = prepareTestRequest(req)
762 + rec := httptest.NewRecorder()
763 + err := badProxy.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
764 + return nil
765 + }))
766 + if err == nil {
767 + t.Fatalf("Expected error from badProxy.ServeHTTP")
768 + }
769 +
770 + // Simulate the Subroute's Error handler by calling another handler with the
771 + // same request and recorder
772 + goodProxy := minimalHandler(0, &Upstream{Host: new(Host), Dial: goodServer.Listener.Addr().String()})
773 + err = goodProxy.ServeHTTP(rec, req, caddyhttp.HandlerFunc(func(w http.ResponseWriter, r *http.Request) error {
774 + return nil
775 + }))
776 +
777 + if err != nil {
778 + t.Fatalf("Expected no error from goodProxy.ServeHTTP, got: %v", err)
779 + }
780 + if rec.Code != http.StatusOK {
781 + t.Errorf("status: got %d, want %d", rec.Code, http.StatusOK)
782 + }
783 + expectedBody := "hello world"
784 + if rec.Body.String() != expectedBody {
785 + t.Errorf("body: got %q, want %q", rec.Body.String(), expectedBody)
786 + }
787 +}
modules/caddyhttp/reverseproxy/reverseproxy.go
+8 -9
@@ -488,20 +488,19 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
488 reqHost := clonedReq.Host
489 reqHeader := clonedReq.Header
490
491 - // When retries are configured and there is a body, wrap it in
492 - // io.NopCloser to prevent Go's transport from closing it on dial
493 - // errors. cloneRequest does a shallow copy, so clonedReq.Body and
491 + // If the request contained a body, wrap it in io.NopCloser
492 + // to prevent Go's transport from closing it on dial errors.
493 + // cloneRequest does a shallow copy, so clonedReq.Body and
494 // r.Body share the same io.ReadCloser — a dial-failure Close()
495 - // would kill the original body for all subsequent retry attempts.
496 - // The real body is closed by the HTTP server when the handler
497 - // returns.
495 + // would kill the original body for all subsequent retry
496 + // attempts or subsequent handlers. The real body is closed by
497 + // the HTTP server when the handler returns.
498 //
499 // If the body was already fully buffered (via request_buffers),
500 // we also extract the buffer so the retry loop can replay it
501 - // from the beginning on each attempt. (see #6259, #7546)
501 + // from the beginning on each attempt. (see #6259, #7546, #7713)
502 var bufferedReqBody *bytes.Buffer
503 - if clonedReq.Body != nil && h.LoadBalancing != nil &&
504 - (h.LoadBalancing.Retries > 0 || h.LoadBalancing.TryDuration > 0) {
503 + if clonedReq.Body != nil {
504 if reqBodyBuf, ok := clonedReq.Body.(bodyReadCloser); ok && reqBodyBuf.body == nil && reqBodyBuf.buf != nil {
505 bufferedReqBody = reqBodyBuf.buf
506 reqBodyBuf.buf = nil