Fix a crash and state transitions when handler closes while client still open
From the commit:
// This previously crashed (reported by Mathieu Lonjaret as observed
// while using Camlistore) because we got a DATA frame from the client
// after the handler exited and our logic at the time was wrong,
// keeping a stream in the map in stateClosed, which tickled an
// invariant check later when we tried to remove that stream (via
// defer sc.closeAllStreamsOnConnClose) when the serverConn serve loop
// ended.
Also adding a possible TODO at top:
// TODO (maybe): add a mechanism for Handlers to going into half-closed-local
// mode (rw.(io.Closer) test?) but not exit their handler, and
// continue to be able to read from the Request.Body. This would be a
// somewhat semantic change from HTTP/1 (or at least what we expose in
// net/http), so I'd probably want to add it there
// too. For now, this package says that returning from the Handler
// ServeHTTP function means you're both done reading and done writing,
// without a way to stop just one or the other.
diff --git a/server.go b/server.go
index 256aa9c..ea8138c 100644
--- a/server.go
+++ b/server.go
@@ -55,6 +55,7 @@
var (
testHookOnConn func()
testHookGetServerConn func(*serverConn)
+ testHookOnPanicMu *sync.Mutex // nil except in tests
testHookOnPanic func(sc *serverConn, panicVal interface{}) (rePanic bool)
)
@@ -76,6 +77,15 @@
// Handlers running) and not be woken up again until the PING packet
// returns.
+// TODO (maybe): add a mechanism for Handlers to going into
+// half-closed-local mode (rw.(io.Closer) test?) but not exit their
+// handler, and continue to be able to read from the
+// Request.Body. This would be a somewhat semantic change from HTTP/1
+// (or at least what we expose in net/http), so I'd probably want to
+// add it there too. For now, this package says that returning from
+// the Handler ServeHTTP function means you're both done reading and
+// done writing, without a way to stop just one or the other.
+
// Server is an HTTP/2 server.
type Server struct {
// MaxHandlers limits the number of http.Handler ServeHTTP goroutines
@@ -452,6 +462,10 @@
}
func (sc *serverConn) notePanic() {
+ if testHookOnPanicMu != nil {
+ testHookOnPanicMu.Lock()
+ defer testHookOnPanicMu.Unlock()
+ }
if testHookOnPanic != nil {
if e := recover(); e != nil {
if testHookOnPanic(sc, e) {
@@ -642,7 +656,17 @@
}
switch st.state {
case stateOpen:
- st.state = stateHalfClosedLocal
+ // Here we would go to stateHalfClosedLocal in
+ // theory, but since our handler is done and
+ // the net/http package provides no mechanism
+ // for finishing writing to a ResponseWriter
+ // while still reading data (see possible TODO
+ // at top of this file), we go into closed
+ // state here anyway, after telling the peer
+ // we're hanging up on them.
+ st.state = stateHalfClosedLocal // won't last long, but necessary for closeStream via resetStream
+ errCancel := StreamError{st.id, ErrCodeCancel}
+ sc.resetStream(errCancel)
case stateHalfClosedRemote:
sc.closeStream(st, nil)
}
@@ -720,13 +744,11 @@
func (sc *serverConn) resetStream(se StreamError) {
sc.serveG.check()
- st, ok := sc.streams[se.StreamID]
- if !ok {
- panic("internal package error; resetStream called on non-existent stream")
- }
sc.writeFrame(frameWriteMsg{write: se})
- st.sentReset = true
- sc.closeStream(st, se)
+ if st, ok := sc.streams[se.StreamID]; ok {
+ st.sentReset = true
+ sc.closeStream(st, se)
+ }
}
// curHeaderStreamID returns the stream ID of the header block we're
@@ -911,7 +933,7 @@
func (sc *serverConn) closeStream(st *stream, err error) {
sc.serveG.check()
if st.state == stateIdle || st.state == stateClosed {
- panic("invariant")
+ panic(fmt.Sprintf("invariant; can't close stream in state %v", st.state))
}
st.state = stateClosed
sc.curOpenStreams--
@@ -1006,7 +1028,12 @@
// with a stream error (Section 5.4.2) of type STREAM_CLOSED."
id := f.Header().StreamID
st, ok := sc.streams[id]
- if !ok || (st.state != stateOpen && st.state != stateHalfClosedLocal) {
+ if !ok || st.state != stateOpen {
+ // This includes sending a RST_STREAM if the stream is
+ // in stateHalfClosedLocal (which currently means that
+ // the http.Handler returned, so it's done reading &
+ // done writing). Try to stop the client from sending
+ // more DATA.
return StreamError{id, ErrCodeStreamClosed}
}
if st.body == nil {
@@ -1043,15 +1070,7 @@
} else {
st.body.Close(io.EOF)
}
- switch st.state {
- case stateOpen:
- st.state = stateHalfClosedRemote
- case stateHalfClosedLocal:
- // TODO: this causes a known crash (currently skipped
- // test in server_test.go). We shouldn't leave
- // streams in the map in stateClosed.
- st.state = stateClosed
- }
+ st.state = stateHalfClosedRemote
}
return nil
}