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