net/context/ctxhttp: fix case where Body could leak and not be closed

Fixes golang/go#14065

Change-Id: Ic19a0f740cddced8fb782f65cea14da383b047b1
Reviewed-on: https://go-review.googlesource.com/18802
Reviewed-by: Olivier Poitrey <rs@rhapsodyk.net>
Reviewed-by: Daniel Morsing <daniel.morsing@gmail.com>
Reviewed-by: Chris Broadfoot <cbro@golang.org>
Run-TryBot: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/context/ctxhttp/ctxhttp.go b/context/ctxhttp/ctxhttp.go
index f434f44..62620d4 100644
--- a/context/ctxhttp/ctxhttp.go
+++ b/context/ctxhttp/ctxhttp.go
@@ -14,6 +14,14 @@
 	"golang.org/x/net/context"
 )
 
+func nop() {}
+
+var (
+	testHookContextDoneBeforeHeaders = nop
+	testHookDoReturned               = nop
+	testHookDidBodyClose             = nop
+)
+
 // Do sends an HTTP request with the provided http.Client and returns an HTTP response.
 // If the client is nil, http.DefaultClient is used.
 // If the context is canceled or times out, ctx.Err() will be returned.
@@ -33,6 +41,7 @@
 
 	go func() {
 		resp, err := client.Do(req)
+		testHookDoReturned()
 		result <- responseAndError{resp, err}
 	}()
 
@@ -40,7 +49,15 @@
 
 	select {
 	case <-ctx.Done():
+		testHookContextDoneBeforeHeaders()
 		cancel()
+		// Clean up after the goroutine calling client.Do:
+		go func() {
+			if r := <-result; r.resp != nil {
+				testHookDidBodyClose()
+				r.resp.Body.Close()
+			}
+		}()
 		return nil, ctx.Err()
 	case r := <-result:
 		var err error
diff --git a/context/ctxhttp/ctxhttp_test.go b/context/ctxhttp/ctxhttp_test.go
index 139fed4..77c25ba 100644
--- a/context/ctxhttp/ctxhttp_test.go
+++ b/context/ctxhttp/ctxhttp_test.go
@@ -8,8 +8,10 @@
 
 import (
 	"io/ioutil"
+	"net"
 	"net/http"
 	"net/http/httptest"
+	"sync"
 	"testing"
 	"time"
 
@@ -111,3 +113,64 @@
 
 	return Get(ctx, nil, serv.URL)
 }
+
+// golang.org/issue/14065
+func TestClosesResponseBodyOnCancel(t *testing.T) {
+	defer func() { testHookContextDoneBeforeHeaders = nop }()
+	defer func() { testHookDoReturned = nop }()
+	defer func() { testHookDidBodyClose = nop }()
+
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
+	defer ts.Close()
+
+	ctx, cancel := context.WithCancel(context.Background())
+
+	// closed when Do enters select case <-ctx.Done()
+	enteredDonePath := make(chan struct{})
+
+	testHookContextDoneBeforeHeaders = func() {
+		close(enteredDonePath)
+	}
+
+	testHookDoReturned = func() {
+		// We now have the result (the Flush'd headers) at least,
+		// so we can cancel the request.
+		cancel()
+
+		// But block the client.Do goroutine from sending
+		// until Do enters into the <-ctx.Done() path, since
+		// otherwise if both channels are readable, select
+		// picks a random one.
+		<-enteredDonePath
+	}
+
+	sawBodyClose := make(chan struct{})
+	testHookDidBodyClose = func() { close(sawBodyClose) }
+
+	tr := &http.Transport{}
+	defer tr.CloseIdleConnections()
+	c := &http.Client{Transport: tr}
+	req, _ := http.NewRequest("GET", ts.URL, nil)
+	_, doErr := Do(ctx, c, req)
+
+	select {
+	case <-sawBodyClose:
+	case <-time.After(5 * time.Second):
+		t.Fatal("timeout waiting for body to close")
+	}
+
+	if doErr != ctx.Err() {
+		t.Errorf("Do error = %v; want %v", doErr, ctx.Err())
+	}
+}
+
+type noteCloseConn struct {
+	net.Conn
+	onceClose sync.Once
+	closefn   func()
+}
+
+func (c *noteCloseConn) Close() error {
+	c.onceClose.Do(c.closefn)
+	return c.Conn.Close()
+}