"net/url"
"strings"
"sync"
+ "sync/atomic"
"time"
"golang.org/x/net/http/httpguts"
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
}
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)
+}
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)