Skip to content

Commit 2369013

Browse files
committed
net/http: fix unwanted HTTP/2 conn Transport crash after IdleConnTimeout
Go 1.7 crashed after Transport.IdleConnTimeout if an HTTP/2 connection was established but but its caller no longer wanted it. (Assuming the connection cache was enabled, which it is by default) Fixes #16208 Change-Id: I9628757f7669e344f416927c77f00ed3864839e3 Reviewed-on: https://go-review.googlesource.com/27450 Reviewed-by: Andrew Gerrand <[email protected]> Run-TryBot: Brad Fitzpatrick <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent 82c1e22 commit 2369013

File tree

2 files changed

+59
-0
lines changed

2 files changed

+59
-0
lines changed

src/net/http/transport.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,7 @@ var (
585585
errReadLoopExiting = errors.New("http: persistConn.readLoop exiting")
586586
errServerClosedIdle = errors.New("http: server closed idle connection")
587587
errIdleConnTimeout = errors.New("http: idle connection timeout")
588+
errNotCachingH2Conn = errors.New("http: not caching alternate protocol's connections")
588589
)
589590

590591
// transportReadFromServerError is used by Transport.readLoop when the
@@ -628,6 +629,9 @@ func (t *Transport) tryPutIdleConn(pconn *persistConn) error {
628629
if pconn.isBroken() {
629630
return errConnBroken
630631
}
632+
if pconn.alt != nil {
633+
return errNotCachingH2Conn
634+
}
631635
pconn.markReused()
632636
key := pconn.cacheKey
633637

src/net/http/transport_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3511,6 +3511,61 @@ func TestTransportIdleConnTimeout(t *testing.T) {
35113511
}
35123512
}
35133513

3514+
// Issue 16208: Go 1.7 crashed after Transport.IdleConnTimeout if an
3515+
// HTTP/2 connection was established but but its caller no longer
3516+
// wanted it. (Assuming the connection cache was enabled, which it is
3517+
// by default)
3518+
//
3519+
// This test reproduced the crash by setting the IdleConnTimeout low
3520+
// (to make the test reasonable) and then making a request which is
3521+
// canceled by the DialTLS hook, which then also waits to return the
3522+
// real connection until after the RoundTrip saw the error. Then we
3523+
// know the successful tls.Dial from DialTLS will need to go into the
3524+
// idle pool. Then we give it a of time to explode.
3525+
func TestIdleConnH2Crash(t *testing.T) {
3526+
cst := newClientServerTest(t, h2Mode, HandlerFunc(func(w ResponseWriter, r *Request) {
3527+
// nothing
3528+
}))
3529+
defer cst.close()
3530+
3531+
ctx, cancel := context.WithCancel(context.Background())
3532+
defer cancel()
3533+
3534+
gotErr := make(chan bool, 1)
3535+
3536+
cst.tr.IdleConnTimeout = 5 * time.Millisecond
3537+
cst.tr.DialTLS = func(network, addr string) (net.Conn, error) {
3538+
cancel()
3539+
<-gotErr
3540+
c, err := tls.Dial(network, addr, &tls.Config{
3541+
InsecureSkipVerify: true,
3542+
NextProtos: []string{"h2"},
3543+
})
3544+
if err != nil {
3545+
t.Error(err)
3546+
return nil, err
3547+
}
3548+
if cs := c.ConnectionState(); cs.NegotiatedProtocol != "h2" {
3549+
t.Errorf("protocol = %q; want %q", cs.NegotiatedProtocol, "h2")
3550+
c.Close()
3551+
return nil, errors.New("bogus")
3552+
}
3553+
return c, nil
3554+
}
3555+
3556+
req, _ := NewRequest("GET", cst.ts.URL, nil)
3557+
req = req.WithContext(ctx)
3558+
res, err := cst.c.Do(req)
3559+
if err == nil {
3560+
res.Body.Close()
3561+
t.Fatal("unexpected success")
3562+
}
3563+
gotErr <- true
3564+
3565+
// Wait for the explosion.
3566+
time.Sleep(cst.tr.IdleConnTimeout * 10)
3567+
}
3568+
35143569
type funcConn struct {
35153570
net.Conn
35163571
read func([]byte) (int, error)

0 commit comments

Comments
 (0)