diff options
Diffstat (limited to 'src/net/http')
-rw-r--r-- | src/net/http/httputil/reverseproxy.go | 9 | ||||
-rw-r--r-- | src/net/http/httputil/reverseproxy_test.go | 39 | ||||
-rw-r--r-- | src/net/http/transport_test.go | 77 |
3 files changed, 99 insertions, 26 deletions
diff --git a/src/net/http/httputil/reverseproxy.go b/src/net/http/httputil/reverseproxy.go index 5d39955d62..8b63368386 100644 --- a/src/net/http/httputil/reverseproxy.go +++ b/src/net/http/httputil/reverseproxy.go @@ -235,6 +235,15 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { if req.ContentLength == 0 { outreq.Body = nil // Issue 16036: nil Body for http.Transport retries } + if outreq.Body != nil { + // 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 + // Issue 46866). Although calling Close doesn't guarantee there isn't + // any Read in flight after the handle returns, in practice it's safe to + // read after closing it. + defer outreq.Body.Close() + } if outreq.Header == nil { outreq.Header = make(http.Header) // Issue 33142: historical behavior was to always allocate } diff --git a/src/net/http/httputil/reverseproxy_test.go b/src/net/http/httputil/reverseproxy_test.go index 1898ed8b8a..4b6ad77a29 100644 --- a/src/net/http/httputil/reverseproxy_test.go +++ b/src/net/http/httputil/reverseproxy_test.go @@ -1122,6 +1122,45 @@ func TestReverseProxy_PanicBodyError(t *testing.T) { rproxy.ServeHTTP(httptest.NewRecorder(), req) } +// Issue #46866: panic without closing incoming request body causes a panic +func TestReverseProxy_PanicClosesIncomingBody(t *testing.T) { + backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + out := "this call was relayed by the reverse proxy" + // Coerce a wrong content length to induce io.ErrUnexpectedEOF + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(out)*2)) + fmt.Fprintln(w, out) + })) + defer backend.Close() + backendURL, err := url.Parse(backend.URL) + if err != nil { + t.Fatal(err) + } + proxyHandler := NewSingleHostReverseProxy(backendURL) + proxyHandler.ErrorLog = log.New(io.Discard, "", 0) // quiet for tests + frontend := httptest.NewServer(proxyHandler) + defer frontend.Close() + frontendClient := frontend.Client() + + var wg sync.WaitGroup + for i := 0; i < 2; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 10; j++ { + const reqLen = 6 * 1024 * 1024 + req, _ := http.NewRequest("POST", frontend.URL, &io.LimitedReader{R: neverEnding('x'), N: reqLen}) + req.ContentLength = reqLen + resp, _ := frontendClient.Transport.RoundTrip(req) + if resp != nil { + io.Copy(io.Discard, resp.Body) + resp.Body.Close() + } + } + }() + } + wg.Wait() +} + func TestSelectFlushInterval(t *testing.T) { tests := []struct { name string diff --git a/src/net/http/transport_test.go b/src/net/http/transport_test.go index 690e0c299d..eeaa492644 100644 --- a/src/net/http/transport_test.go +++ b/src/net/http/transport_test.go @@ -6441,10 +6441,11 @@ func TestErrorWriteLoopRace(t *testing.T) { // Test that a new request which uses the connection of an active request // cannot cause it to be canceled as well. func TestCancelRequestWhenSharingConnection(t *testing.T) { - if testing.Short() { - t.Skip("skipping in short mode") - } + reqc := make(chan chan struct{}, 2) ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, req *Request) { + ch := make(chan struct{}, 1) + reqc <- ch + <-ch w.Header().Add("Content-Length", "0") })) defer ts.Close() @@ -6456,34 +6457,58 @@ func TestCancelRequestWhenSharingConnection(t *testing.T) { var wg sync.WaitGroup - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + wg.Add(1) + putidlec := make(chan chan struct{}) + go func() { + defer wg.Done() + ctx := httptrace.WithClientTrace(context.Background(), &httptrace.ClientTrace{ + PutIdleConn: func(error) { + // Signal that the idle conn has been returned to the pool, + // and wait for the order to proceed. + ch := make(chan struct{}) + putidlec <- ch + <-ch + }, + }) + req, _ := NewRequestWithContext(ctx, "GET", ts.URL, nil) + res, err := client.Do(req) + if err == nil { + res.Body.Close() + } + if err != nil { + t.Errorf("request 1: got err %v, want nil", err) + } + }() - for i := 0; i < 10; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for ctx.Err() == nil { - reqctx, reqcancel := context.WithCancel(ctx) - go reqcancel() - req, _ := NewRequestWithContext(reqctx, "GET", ts.URL, nil) - res, err := client.Do(req) - if err == nil { - res.Body.Close() - } - } - }() - } + // Wait for the first request to receive a response and return the + // connection to the idle pool. + r1c := <-reqc + close(r1c) + idlec := <-putidlec - for ctx.Err() == nil { - req, _ := NewRequest("GET", ts.URL, nil) - if res, err := client.Do(req); err != nil { - t.Errorf("unexpected: %p %v", req, err) - break - } else { + wg.Add(1) + cancelctx, cancel := context.WithCancel(context.Background()) + go func() { + defer wg.Done() + req, _ := NewRequestWithContext(cancelctx, "GET", ts.URL, nil) + res, err := client.Do(req) + if err == nil { res.Body.Close() } - } + if !errors.Is(err, context.Canceled) { + t.Errorf("request 2: got err %v, want Canceled", err) + } + }() + // Wait for the second request to arrive at the server, and then cancel + // the request context. + r2c := <-reqc cancel() + + // Give the cancelation a moment to take effect, and then unblock the first request. + time.Sleep(1 * time.Millisecond) + close(idlec) + + close(r2c) wg.Wait() } |