diff options
Diffstat (limited to 'src/net/http')
| -rw-r--r-- | src/net/http/cookie.go | 10 | ||||
| -rw-r--r-- | src/net/http/cookie_test.go | 32 | ||||
| -rw-r--r-- | src/net/http/export_test.go | 25 | ||||
| -rw-r--r-- | src/net/http/httputil/dump.go | 12 | ||||
| -rw-r--r-- | src/net/http/httputil/dump_test.go | 30 | ||||
| -rw-r--r-- | src/net/http/main_test.go (renamed from src/net/http/z_last_test.go) | 30 | ||||
| -rw-r--r-- | src/net/http/request.go | 3 | ||||
| -rw-r--r-- | src/net/http/serve_test.go | 52 | ||||
| -rw-r--r-- | src/net/http/server.go | 37 | ||||
| -rw-r--r-- | src/net/http/transport.go | 46 | ||||
| -rw-r--r-- | src/net/http/transport_test.go | 90 |
11 files changed, 325 insertions, 42 deletions
diff --git a/src/net/http/cookie.go b/src/net/http/cookie.go index dc60ba87f5..a0d0fdbbd0 100644 --- a/src/net/http/cookie.go +++ b/src/net/http/cookie.go @@ -56,7 +56,7 @@ func readSetCookies(h Header) []*Cookie { if !isCookieNameValid(name) { continue } - value, success := parseCookieValue(value) + value, success := parseCookieValue(value, true) if !success { continue } @@ -76,7 +76,7 @@ func readSetCookies(h Header) []*Cookie { attr, val = attr[:j], attr[j+1:] } lowerAttr := strings.ToLower(attr) - val, success = parseCookieValue(val) + val, success = parseCookieValue(val, false) if !success { c.Unparsed = append(c.Unparsed, parts[i]) continue @@ -205,7 +205,7 @@ func readCookies(h Header, filter string) []*Cookie { if filter != "" && filter != name { continue } - val, success := parseCookieValue(val) + val, success := parseCookieValue(val, true) if !success { continue } @@ -345,9 +345,9 @@ func sanitizeOrWarn(fieldName string, valid func(byte) bool, v string) string { return string(buf) } -func parseCookieValue(raw string) (string, bool) { +func parseCookieValue(raw string, allowDoubleQuote bool) (string, bool) { // Strip the quotes, if present. - if len(raw) > 1 && raw[0] == '"' && raw[len(raw)-1] == '"' { + if allowDoubleQuote && len(raw) > 1 && raw[0] == '"' && raw[len(raw)-1] == '"' { raw = raw[1 : len(raw)-1] } for i := 0; i < len(raw); i++ { diff --git a/src/net/http/cookie_test.go b/src/net/http/cookie_test.go index f78f37299f..98dc2fade0 100644 --- a/src/net/http/cookie_test.go +++ b/src/net/http/cookie_test.go @@ -313,6 +313,14 @@ var readCookiesTests = []struct { {Name: "c2", Value: "v2"}, }, }, + { + Header{"Cookie": {`Cookie-1="v$1"; c2="v2"`}}, + "", + []*Cookie{ + {Name: "Cookie-1", Value: "v$1"}, + {Name: "c2", Value: "v2"}, + }, + }, } func TestReadCookies(t *testing.T) { @@ -327,6 +335,30 @@ func TestReadCookies(t *testing.T) { } } +func TestSetCookieDoubleQuotes(t *testing.T) { + res := &Response{Header: Header{}} + res.Header.Add("Set-Cookie", `quoted0=none; max-age=30`) + res.Header.Add("Set-Cookie", `quoted1="cookieValue"; max-age=31`) + res.Header.Add("Set-Cookie", `quoted2=cookieAV; max-age="32"`) + res.Header.Add("Set-Cookie", `quoted3="both"; max-age="33"`) + got := res.Cookies() + want := []*Cookie{ + {Name: "quoted0", Value: "none", MaxAge: 30}, + {Name: "quoted1", Value: "cookieValue", MaxAge: 31}, + {Name: "quoted2", Value: "cookieAV"}, + {Name: "quoted3", Value: "both"}, + } + if len(got) != len(want) { + t.Fatal("got %d cookies, want %d", len(got), len(want)) + } + for i, w := range want { + g := got[i] + if g.Name != w.Name || g.Value != w.Value || g.MaxAge != w.MaxAge { + t.Errorf("cookie #%d:\ngot %v\nwant %v", i, g, w) + } + } +} + func TestCookieSanitizeValue(t *testing.T) { defer log.SetOutput(os.Stderr) var logbuf bytes.Buffer diff --git a/src/net/http/export_test.go b/src/net/http/export_test.go index 2c87353554..a6980b5389 100644 --- a/src/net/http/export_test.go +++ b/src/net/http/export_test.go @@ -57,6 +57,26 @@ func (t *Transport) IdleConnChMapSizeForTesting() int { return len(t.idleConnCh) } +func (t *Transport) IsIdleForTesting() bool { + t.idleMu.Lock() + defer t.idleMu.Unlock() + return t.wantIdle +} + +func (t *Transport) RequestIdleConnChForTesting() { + t.getIdleConnCh(connectMethod{nil, "http", "example.com"}) +} + +func (t *Transport) PutIdleTestConn() bool { + c, _ := net.Pipe() + return t.putIdleConn(&persistConn{ + t: t, + conn: c, // dummy + closech: make(chan struct{}), // so it can be closed + cacheKey: connectMethodKey{"", "http", "example.com"}, + }) +} + func NewTestTimeoutHandler(handler Handler, ch <-chan time.Time) Handler { f := func() <-chan time.Time { return ch @@ -66,6 +86,7 @@ func NewTestTimeoutHandler(handler Handler, ch <-chan time.Time) Handler { func ResetCachedEnvironment() { httpProxyEnv.reset() + httpsProxyEnv.reset() noProxyEnv.reset() } @@ -76,3 +97,7 @@ var DefaultUserAgent = defaultUserAgent func SetPendingDialHooks(before, after func()) { prePendingDial, postPendingDial = before, after } + +var ExportServerNewConn = (*Server).newConn + +var ExportCloseWriteAndWait = (*conn).closeWriteAndWait diff --git a/src/net/http/httputil/dump.go b/src/net/http/httputil/dump.go index 2a7a413d01..ac8f103f9b 100644 --- a/src/net/http/httputil/dump.go +++ b/src/net/http/httputil/dump.go @@ -95,19 +95,27 @@ func DumpRequestOut(req *http.Request, body bool) ([]byte, error) { // with a dummy response. var buf bytes.Buffer // records the output pr, pw := io.Pipe() + defer pr.Close() + defer pw.Close() dr := &delegateReader{c: make(chan io.Reader)} // Wait for the request before replying with a dummy response: go func() { - http.ReadRequest(bufio.NewReader(pr)) + req, err := http.ReadRequest(bufio.NewReader(pr)) + if err == nil { + // Ensure all the body is read; otherwise + // we'll get a partial dump. + io.Copy(ioutil.Discard, req.Body) + req.Body.Close() + } dr.c <- strings.NewReader("HTTP/1.1 204 No Content\r\n\r\n") }() t := &http.Transport{ + DisableKeepAlives: true, Dial: func(net, addr string) (net.Conn, error) { return &dumpConn{io.MultiWriter(&buf, pw), dr}, nil }, } - defer t.CloseIdleConnections() _, err := t.RoundTrip(reqSend) diff --git a/src/net/http/httputil/dump_test.go b/src/net/http/httputil/dump_test.go index e1ffb3935a..024ee5a86f 100644 --- a/src/net/http/httputil/dump_test.go +++ b/src/net/http/httputil/dump_test.go @@ -111,6 +111,30 @@ var dumpTests = []dumpTest{ NoBody: true, }, + + // Request with Body > 8196 (default buffer size) + { + Req: http.Request{ + Method: "POST", + URL: &url.URL{ + Scheme: "http", + Host: "post.tld", + Path: "/", + }, + ContentLength: 8193, + ProtoMajor: 1, + ProtoMinor: 1, + }, + + Body: bytes.Repeat([]byte("a"), 8193), + + WantDumpOut: "POST / HTTP/1.1\r\n" + + "Host: post.tld\r\n" + + "User-Agent: Go 1.1 package http\r\n" + + "Content-Length: 8193\r\n" + + "Accept-Encoding: gzip\r\n\r\n" + + strings.Repeat("a", 8193), + }, } func TestDumpRequest(t *testing.T) { @@ -125,6 +149,8 @@ func TestDumpRequest(t *testing.T) { tt.Req.Body = ioutil.NopCloser(bytes.NewReader(b)) case func() io.ReadCloser: tt.Req.Body = b() + default: + t.Fatalf("Test %d: unsupported Body of %T", i, tt.Body) } } setBody() @@ -159,7 +185,9 @@ func TestDumpRequest(t *testing.T) { } } if dg := runtime.NumGoroutine() - numg0; dg > 4 { - t.Errorf("Unexpectedly large number of new goroutines: %d new", dg) + buf := make([]byte, 4096) + buf = buf[:runtime.Stack(buf, true)] + t.Errorf("Unexpectedly large number of new goroutines: %d new: %s", dg, buf) } } diff --git a/src/net/http/z_last_test.go b/src/net/http/main_test.go index 5a0cc11984..9f1dfc3727 100644 --- a/src/net/http/z_last_test.go +++ b/src/net/http/main_test.go @@ -5,7 +5,9 @@ package http_test import ( + "fmt" "net/http" + "os" "runtime" "sort" "strings" @@ -13,6 +15,14 @@ import ( "time" ) +func TestMain(m *testing.M) { + v := m.Run() + if v == 0 && goroutineLeaked() { + os.Exit(1) + } + os.Exit(v) +} + func interestingGoroutines() (gs []string) { buf := make([]byte, 2<<20) buf = buf[:runtime.Stack(buf, true)] @@ -30,6 +40,7 @@ func interestingGoroutines() (gs []string) { // These only show up with GOTRACEBACK=2; Issue 5005 (comment 28) strings.Contains(stack, "runtime.goexit") || strings.Contains(stack, "created by runtime.gc") || + strings.Contains(stack, "net/http_test.interestingGoroutines") || strings.Contains(stack, "runtime.MHeap_Scavenger") { continue } @@ -40,10 +51,10 @@ func interestingGoroutines() (gs []string) { } // Verify the other tests didn't leave any goroutines running. -// This is in a file named z_last_test.go so it sorts at the end. -func TestGoroutinesRunning(t *testing.T) { +func goroutineLeaked() bool { if testing.Short() { - t.Skip("not counting goroutines for leakage in -short mode") + // not counting goroutines for leakage in -short mode + return false } gs := interestingGoroutines() @@ -54,13 +65,14 @@ func TestGoroutinesRunning(t *testing.T) { n++ } - t.Logf("num goroutines = %d", n) - if n > 0 { - t.Error("Too many goroutines.") - for stack, count := range stackCount { - t.Logf("%d instances of:\n%s", count, stack) - } + if n == 0 { + return false + } + fmt.Fprintf(os.Stderr, "Too many goroutines running after net/http test(s).\n") + for stack, count := range stackCount { + fmt.Fprintf(os.Stderr, "%d instances of:\n%s", count, stack) } + return true } func afterTest(t *testing.T) { diff --git a/src/net/http/request.go b/src/net/http/request.go index 263c26c9bd..487eebcb84 100644 --- a/src/net/http/request.go +++ b/src/net/http/request.go @@ -852,7 +852,8 @@ func (r *Request) ParseMultipartForm(maxMemory int64) error { // POST and PUT body parameters take precedence over URL query string values. // FormValue calls ParseMultipartForm and ParseForm if necessary and ignores // any errors returned by these functions. -// To access multiple values of the same key use ParseForm. +// To access multiple values of the same key, call ParseForm and +// then inspect Request.Form directly. func (r *Request) FormValue(key string) string { if r.Form == nil { r.ParseMultipartForm(defaultMaxMemory) diff --git a/src/net/http/serve_test.go b/src/net/http/serve_test.go index ee4f204995..702bffdc13 100644 --- a/src/net/http/serve_test.go +++ b/src/net/http/serve_test.go @@ -778,6 +778,35 @@ func TestChunkedResponseHeaders(t *testing.T) { } } +func TestIdentityResponseHeaders(t *testing.T) { + defer afterTest(t) + log.SetOutput(ioutil.Discard) // is noisy otherwise + defer log.SetOutput(os.Stderr) + + ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) { + w.Header().Set("Transfer-Encoding", "identity") + w.(Flusher).Flush() + fmt.Fprintf(w, "I am an identity response.") + })) + defer ts.Close() + + res, err := Get(ts.URL) + if err != nil { + t.Fatalf("Get error: %v", err) + } + defer res.Body.Close() + + if g, e := res.TransferEncoding, []string(nil); !reflect.DeepEqual(g, e) { + t.Errorf("expected TransferEncoding of %v; got %v", e, g) + } + if _, haveCL := res.Header["Content-Length"]; haveCL { + t.Errorf("Unexpected Content-Length") + } + if !res.Close { + t.Errorf("expected Connection: close; got %v", res.Close) + } +} + // Test304Responses verifies that 304s don't declare that they're // chunking in their response headers and aren't allowed to produce // output. @@ -2607,6 +2636,29 @@ func TestServerConnStateNew(t *testing.T) { } } +type closeWriteTestConn struct { + rwTestConn + didCloseWrite bool +} + +func (c *closeWriteTestConn) CloseWrite() error { + c.didCloseWrite = true + return nil +} + +func TestCloseWrite(t *testing.T) { + var srv Server + var testConn closeWriteTestConn + c, err := ExportServerNewConn(&srv, &testConn) + if err != nil { + t.Fatal(err) + } + ExportCloseWriteAndWait(c) + if !testConn.didCloseWrite { + t.Error("didn't see CloseWrite call") + } +} + func BenchmarkClientServer(b *testing.B) { b.ReportAllocs() b.StopTimer() diff --git a/src/net/http/server.go b/src/net/http/server.go index 203037e9f5..b5959f7321 100644 --- a/src/net/http/server.go +++ b/src/net/http/server.go @@ -42,6 +42,12 @@ var ( // and then return. Returning signals that the request is finished // and that the HTTP server can move on to the next request on // the connection. +// +// If ServeHTTP panics, the server (the caller of ServeHTTP) assumes +// that the effect of the panic was isolated to the active request. +// It recovers the panic, logs a stack trace to the server error log, +// and hangs up the connection. +// type Handler interface { ServeHTTP(ResponseWriter, *Request) } @@ -833,13 +839,20 @@ func (cw *chunkWriter) writeHeader(p []byte) { } else if hasCL { delHeader("Transfer-Encoding") } else if w.req.ProtoAtLeast(1, 1) { - // HTTP/1.1 or greater: use chunked transfer encoding - // to avoid closing the connection at EOF. - // TODO: this blows away any custom or stacked Transfer-Encoding they - // might have set. Deal with that as need arises once we have a valid - // use case. - cw.chunking = true - setHeader.transferEncoding = "chunked" + // HTTP/1.1 or greater: Transfer-Encoding has been set to identity, and no + // content-length has been provided. The connection must be closed after the + // reply is written, and no chunking is to be done. This is the setup + // recommended in the Server-Sent Events candidate recommendation 11, + // section 8. + if hasTE && te == "identity" { + cw.chunking = false + w.closeAfterReply = true + } else { + // HTTP/1.1 or greater: use chunked transfer encoding + // to avoid closing the connection at EOF. + cw.chunking = true + setHeader.transferEncoding = "chunked" + } } else { // HTTP version < 1.1: cannot do chunked transfer // encoding and we don't know the Content-Length so @@ -1058,15 +1071,21 @@ func (c *conn) close() { // This timeout is somewhat arbitrary (~latency around the planet). const rstAvoidanceDelay = 500 * time.Millisecond +type closeWriter interface { + CloseWrite() error +} + +var _ closeWriter = (*net.TCPConn)(nil) + // closeWrite flushes any outstanding data and sends a FIN packet (if // client is connected via TCP), signalling that we're done. We then -// pause for a bit, hoping the client processes it before `any +// pause for a bit, hoping the client processes it before any // subsequent RST. // // See http://golang.org/issue/3595 func (c *conn) closeWriteAndWait() { c.finalFlush() - if tcp, ok := c.rwc.(*net.TCPConn); ok { + if tcp, ok := c.rwc.(closeWriter); ok { tcp.CloseWrite() } time.Sleep(rstAvoidanceDelay) diff --git a/src/net/http/transport.go b/src/net/http/transport.go index 527ed8bdd1..70e574fc86 100644 --- a/src/net/http/transport.go +++ b/src/net/http/transport.go @@ -47,13 +47,16 @@ const DefaultMaxIdleConnsPerHost = 2 // HTTPS, and HTTP proxies (for either HTTP or HTTPS with CONNECT). // Transport can also cache connections for future re-use. type Transport struct { - idleMu sync.Mutex - idleConn map[connectMethodKey][]*persistConn - idleConnCh map[connectMethodKey]chan *persistConn + idleMu sync.Mutex + wantIdle bool // user has requested to close all idle conns + idleConn map[connectMethodKey][]*persistConn + idleConnCh map[connectMethodKey]chan *persistConn + reqMu sync.Mutex reqCanceler map[*Request]func() - altMu sync.RWMutex - altProto map[string]RoundTripper // nil or map of URI scheme => RoundTripper + + altMu sync.RWMutex + altProto map[string]RoundTripper // nil or map of URI scheme => RoundTripper // Proxy specifies a function to return a proxy for a given // Request. If the function returns a non-nil error, the @@ -116,15 +119,28 @@ type Transport struct { // ProxyFromEnvironment returns the URL of the proxy to use for a // given request, as indicated by the environment variables -// $HTTP_PROXY and $NO_PROXY (or $http_proxy and $no_proxy). -// An error is returned if the proxy environment is invalid. +// HTTP_PROXY, HTTPS_PROXY and NO_PROXY (or the lowercase versions +// thereof). HTTPS_PROXY takes precedence over HTTP_PROXY for https +// requests. +// +// The environment values may be either a complete URL or a +// "host[:port]", in which case the "http" scheme is assumed. +// An error is returned if the value is a different form. +// // A nil URL and nil error are returned if no proxy is defined in the -// environment, or a proxy should not be used for the given request. +// environment, or a proxy should not be used for the given request, +// as defined by NO_PROXY. // // As a special case, if req.URL.Host is "localhost" (with or without // a port number), then a nil URL and nil error will be returned. func ProxyFromEnvironment(req *Request) (*url.URL, error) { - proxy := httpProxyEnv.Get() + var proxy string + if req.URL.Scheme == "https" { + proxy = httpsProxyEnv.Get() + } + if proxy == "" { + proxy = httpProxyEnv.Get() + } if proxy == "" { return nil, nil } @@ -249,6 +265,7 @@ func (t *Transport) CloseIdleConnections() { m := t.idleConn t.idleConn = nil t.idleConnCh = nil + t.wantIdle = true t.idleMu.Unlock() for _, conns := range m { for _, pconn := range conns { @@ -276,6 +293,9 @@ var ( httpProxyEnv = &envOnce{ names: []string{"HTTP_PROXY", "http_proxy"}, } + httpsProxyEnv = &envOnce{ + names: []string{"HTTPS_PROXY", "https_proxy"}, + } noProxyEnv = &envOnce{ names: []string{"NO_PROXY", "no_proxy"}, } @@ -316,7 +336,7 @@ func (t *Transport) connectMethodForRequest(treq *transportRequest) (cm connectM if t.Proxy != nil { cm.proxyURL, err = t.Proxy(treq.Request) } - return cm, nil + return cm, err } // proxyAuth returns the Proxy-Authorization header to set @@ -369,6 +389,11 @@ func (t *Transport) putIdleConn(pconn *persistConn) bool { delete(t.idleConnCh, key) } } + if t.wantIdle { + t.idleMu.Unlock() + pconn.close() + return false + } if t.idleConn == nil { t.idleConn = make(map[connectMethodKey][]*persistConn) } @@ -397,6 +422,7 @@ func (t *Transport) getIdleConnCh(cm connectMethod) chan *persistConn { key := cm.key() t.idleMu.Lock() defer t.idleMu.Unlock() + t.wantIdle = false if t.idleConnCh == nil { t.idleConnCh = make(map[connectMethodKey]chan *persistConn) } diff --git a/src/net/http/transport_test.go b/src/net/http/transport_test.go index 3460d690e3..66fcc3c7d4 100644 --- a/src/net/http/transport_test.go +++ b/src/net/http/transport_test.go @@ -1701,26 +1701,40 @@ Content-Length: %d } type proxyFromEnvTest struct { - req string // URL to fetch; blank means "http://example.com" - env string - noenv string + req string // URL to fetch; blank means "http://example.com" + + env string // HTTP_PROXY + httpsenv string // HTTPS_PROXY + noenv string // NO_RPXY + want string wanterr error } func (t proxyFromEnvTest) String() string { var buf bytes.Buffer + space := func() { + if buf.Len() > 0 { + buf.WriteByte(' ') + } + } if t.env != "" { fmt.Fprintf(&buf, "http_proxy=%q", t.env) } + if t.httpsenv != "" { + space() + fmt.Fprintf(&buf, "https_proxy=%q", t.httpsenv) + } if t.noenv != "" { - fmt.Fprintf(&buf, " no_proxy=%q", t.noenv) + space() + fmt.Fprintf(&buf, "no_proxy=%q", t.noenv) } req := "http://example.com" if t.req != "" { req = t.req } - fmt.Fprintf(&buf, " req=%q", req) + space() + fmt.Fprintf(&buf, "req=%q", req) return strings.TrimSpace(buf.String()) } @@ -1731,7 +1745,15 @@ var proxyFromEnvTests = []proxyFromEnvTest{ {env: "https://cache.corp.example.com", want: "https://cache.corp.example.com"}, {env: "http://127.0.0.1:8080", want: "http://127.0.0.1:8080"}, {env: "https://127.0.0.1:8080", want: "https://127.0.0.1:8080"}, + + // Don't use secure for http + {req: "http://insecure.tld/", env: "http.proxy.tld", httpsenv: "secure.proxy.tld", want: "http://http.proxy.tld"}, + // Use secure for https. + {req: "https://secure.tld/", env: "http.proxy.tld", httpsenv: "secure.proxy.tld", want: "http://secure.proxy.tld"}, + {req: "https://secure.tld/", env: "http.proxy.tld", httpsenv: "https://secure.proxy.tld", want: "https://secure.proxy.tld"}, + {want: "<nil>"}, + {noenv: "example.com", req: "http://example.com/", env: "proxy", want: "<nil>"}, {noenv: ".example.com", req: "http://example.com/", env: "proxy", want: "<nil>"}, {noenv: "ample.com", req: "http://example.com/", env: "proxy", want: "http://proxy"}, @@ -1743,6 +1765,7 @@ func TestProxyFromEnvironment(t *testing.T) { ResetProxyEnv() for _, tt := range proxyFromEnvTests { os.Setenv("HTTP_PROXY", tt.env) + os.Setenv("HTTPS_PROXY", tt.httpsenv) os.Setenv("NO_PROXY", tt.noenv) ResetCachedEnvironment() reqURL := tt.req @@ -2136,6 +2159,63 @@ func TestTransportDialTLS(t *testing.T) { } } +// Test for issue 8755 +// Ensure that if a proxy returns an error, it is exposed by RoundTrip +func TestRoundTripReturnsProxyError(t *testing.T) { + badProxy := func(*http.Request) (*url.URL, error) { + return nil, errors.New("errorMessage") + } + + tr := &Transport{Proxy: badProxy} + + req, _ := http.NewRequest("GET", "http://example.com", nil) + + _, err := tr.RoundTrip(req) + + if err == nil { + t.Error("Expected proxy error to be returned by RoundTrip") + } +} + +// tests that putting an idle conn after a call to CloseIdleConns does return it +func TestTransportCloseIdleConnsThenReturn(t *testing.T) { + tr := &Transport{} + wantIdle := func(when string, n int) bool { + got := tr.IdleConnCountForTesting("|http|example.com") // key used by PutIdleTestConn + if got == n { + return true + } + t.Errorf("%s: idle conns = %d; want %d", when, got, n) + return false + } + wantIdle("start", 0) + if !tr.PutIdleTestConn() { + t.Fatal("put failed") + } + if !tr.PutIdleTestConn() { + t.Fatal("second put failed") + } + wantIdle("after put", 2) + tr.CloseIdleConnections() + if !tr.IsIdleForTesting() { + t.Error("should be idle after CloseIdleConnections") + } + wantIdle("after close idle", 0) + if tr.PutIdleTestConn() { + t.Fatal("put didn't fail") + } + wantIdle("after second put", 0) + + tr.RequestIdleConnChForTesting() // should toggle the transport out of idle mode + if tr.IsIdleForTesting() { + t.Error("shouldn't be idle after RequestIdleConnChForTesting") + } + if !tr.PutIdleTestConn() { + t.Fatal("after re-activation") + } + wantIdle("after final put", 1) +} + func wantBody(res *http.Response, err error, want string) error { if err != nil { return err |
