diff --git a/http2/go17.go b/http2/go17.go index e3fb4129..47b7fae0 100644 --- a/http2/go17.go +++ b/http2/go17.go @@ -39,6 +39,13 @@ type clientTrace httptrace.ClientTrace func reqContext(r *http.Request) context.Context { return r.Context() } +func (t *Transport) idleConnTimeout() time.Duration { + if t.t1 != nil { + return t.t1.IdleConnTimeout + } + return 0 +} + func setResponseUncompressed(res *http.Response) { res.Uncompressed = true } func traceGotConn(req *http.Request, cc *ClientConn) { diff --git a/http2/not_go17.go b/http2/not_go17.go index 187f33ce..140434a7 100644 --- a/http2/not_go17.go +++ b/http2/not_go17.go @@ -10,6 +10,7 @@ import ( "crypto/tls" "net" "net/http" + "time" ) type contextContext interface { @@ -82,3 +83,5 @@ func cloneTLSConfig(c *tls.Config) *tls.Config { func (cc *ClientConn) Ping(ctx contextContext) error { return cc.ping(ctx) } + +func (t *Transport) idleConnTimeout() time.Duration { return 0 } diff --git a/http2/transport.go b/http2/transport.go index 96511309..165d4742 100644 --- a/http2/transport.go +++ b/http2/transport.go @@ -151,6 +151,9 @@ type ClientConn struct { readerDone chan struct{} // closed on error readerErr error // set before readerDone is closed + idleTimeout time.Duration // or 0 for never + idleTimer *time.Timer + mu sync.Mutex // guards following cond *sync.Cond // hold mu; broadcast on flow/closed changes flow flow // our conn-level flow control quota (cs.flow is per stream) @@ -435,6 +438,10 @@ func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, erro wantSettingsAck: true, pings: make(map[[8]byte]chan struct{}), } + if d := t.idleConnTimeout(); d != 0 { + cc.idleTimeout = d + cc.idleTimer = time.AfterFunc(d, cc.onIdleTimeout) + } if VerboseLogs { t.vlogf("http2: Transport creating client conn %p to %v", cc, c.RemoteAddr()) } @@ -511,6 +518,16 @@ func (cc *ClientConn) canTakeNewRequestLocked() bool { cc.nextStreamID < math.MaxInt32 } +// onIdleTimeout is called from a time.AfterFunc goroutine. It will +// only be called when we're idle, but because we're coming from a new +// goroutine, there could be a new request coming in at the same time, +// so this simply calls the synchronized closeIfIdle to shut down this +// connection. The timer could just call closeIfIdle, but this is more +// clear. +func (cc *ClientConn) onIdleTimeout() { + cc.closeIfIdle() +} + func (cc *ClientConn) closeIfIdle() { cc.mu.Lock() if len(cc.streams) > 0 { @@ -652,6 +669,9 @@ func (cc *ClientConn) RoundTrip(req *http.Request) (*http.Response, error) { if err := checkConnHeaders(req); err != nil { return nil, err } + if cc.idleTimer != nil { + cc.idleTimer.Stop() + } trailers, err := commaSeparatedTrailers(req) if err != nil { @@ -1176,6 +1196,9 @@ func (cc *ClientConn) streamByID(id uint32, andRemove bool) *clientStream { if andRemove && cs != nil && !cc.closed { cc.lastActive = time.Now() delete(cc.streams, id) + if len(cc.streams) == 0 && cc.idleTimer != nil { + cc.idleTimer.Reset(cc.idleTimeout) + } close(cs.done) cc.cond.Broadcast() // wake up checkResetOrDone via clientStream.awaitFlowControl } @@ -1232,6 +1255,10 @@ func (rl *clientConnReadLoop) cleanup() { defer cc.t.connPool().MarkDead(cc) defer close(cc.readerDone) + if cc.idleTimer != nil { + cc.idleTimer.Stop() + } + // Close any response bodies if the server closes prematurely. // TODO: also do this if we've written the headers but not // gotten a response yet.