aboutsummaryrefslogtreecommitdiff
path: root/src/net/http
diff options
context:
space:
mode:
Diffstat (limited to 'src/net/http')
-rw-r--r--src/net/http/httputil/reverseproxy.go30
-rw-r--r--src/net/http/httputil/reverseproxy_test.go38
2 files changed, 68 insertions, 0 deletions
diff --git a/src/net/http/httputil/reverseproxy.go b/src/net/http/httputil/reverseproxy.go
index 9d8784cd2b..7c220319c4 100644
--- a/src/net/http/httputil/reverseproxy.go
+++ b/src/net/http/httputil/reverseproxy.go
@@ -21,6 +21,7 @@ import (
"net/url"
"strings"
"sync"
+ "sync/atomic"
"time"
"golang.org/x/net/http/httpguts"
@@ -435,6 +436,18 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
outreq.Body = nil // Issue 16036: nil Body for http.Transport retries
}
if outreq.Body != nil {
+ // Wrap the body in a reader where Close does nothing. This is done
+ // because p.Transport.RoundTrip would close the reverse proxy's
+ // outbound request body if it fails to connect to upstream. If we do
+ // not wrap the body, when we close the reverse proxy's outbound
+ // request, it will also close the reverse proxy's inbound request body
+ // (i.e. the client's outbound request body). This is because
+ // http.(*Request).Clone creates a shallow copy of the body. This can
+ // cause an infinite hang in cases where the body is not yet received
+ // from the client (e.g. 100-continue requests): Close, which
+ // internally tries to consume the body content, would be called too
+ // early and would hang.
+ outreq.Body = &noopCloseReader{readCloser: outreq.Body}
// Reading from the request body after returning from a handler is not
// allowed, and the RoundTrip goroutine that reads the Body can outlive
// this handler. This can lead to a crash if the handler panics (see
@@ -941,3 +954,20 @@ func ishex(c byte) bool {
}
return false
}
+
+type noopCloseReader struct {
+ readCloser io.ReadCloser
+ closed atomic.Bool
+}
+
+func (ncr *noopCloseReader) Close() error {
+ ncr.closed.Store(true)
+ return nil
+}
+
+func (ncr *noopCloseReader) Read(p []byte) (int, error) {
+ if ncr.closed.Load() {
+ return 0, errors.New("ReverseProxy does an invalid Read on closed Body")
+ }
+ return ncr.readCloser.Read(p)
+}
diff --git a/src/net/http/httputil/reverseproxy_test.go b/src/net/http/httputil/reverseproxy_test.go
index 62c93fb261..3cca6eda99 100644
--- a/src/net/http/httputil/reverseproxy_test.go
+++ b/src/net/http/httputil/reverseproxy_test.go
@@ -2149,6 +2149,44 @@ func TestReverseProxyHijackCopyError(t *testing.T) {
proxyHandler.ServeHTTP(rw, req)
}
+// https://go.dev/issue/75933.
+func TestReverseProxyInvalidUpstream100ContinueDoNotHang(t *testing.T) {
+ proxy := ReverseProxy{
+ Transport: &http.Transport{DisableKeepAlives: true, ExpectContinueTimeout: time.Second * 60},
+ Director: func(request *http.Request) {
+ request.URL.Scheme = "http"
+ request.URL.Host = "doesnotexist:12345" // non-existent upstream
+ },
+ }
+ handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ proxy.ServeHTTP(w, r)
+ })
+ upstreamServer := httptest.NewServer(handler)
+ defer upstreamServer.Close()
+
+ conn, err := net.Dial("tcp", upstreamServer.Listener.Addr().String())
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer conn.Close()
+
+ requestBody := `{"test": "data"}`
+ initialRequest := fmt.Sprintf("POST %s/test-expect HTTP/1.1\r\n"+
+ "Host: %s\r\n"+
+ "Content-Type: application/json\r\n"+
+ "Content-Length: %d\r\n"+
+ "Expect: 100-continue\r\n"+
+ "\r\n", upstreamServer.URL, upstreamServer.Listener.Addr().String(), len(requestBody))
+
+ if _, err := conn.Write([]byte(initialRequest)); err != nil {
+ log.Fatal(err)
+ }
+ buff := make([]byte, 1024)
+ if _, err := conn.Read(buff); err != nil {
+ log.Fatal(err)
+ }
+}
+
type testResponseWriter struct {
h http.Header
writeHeader func(int)