src/net/http/internal/http2/flow.go | 103 ++++++++++++++++++++++++++++++++++++++++------------- src/net/http/internal/http2/flow_test.go | 37 +++++++++++++++++++------------------ src/net/http/internal/http2/server.go | 49 ++++++++++++++++++++++--------------------------- src/net/http/internal/http2/server_test.go | 19 +++++++++++++++++++ src/net/http/internal/http2/transport.go | 104 +++++++++++++++++++++++++++++++++-------------------- src/net/http/internal/http2/transport_test.go | 21 +++++++++++++++++++-- src/net/http/internal/http2/writesched.go | 6 +++++- src/net/http/internal/http2/writesched_test.go | 3 +++ diff --git a/src/net/http/internal/http2/flow.go b/src/net/http/internal/http2/flow.go index b7dbd186957ef718b84b4000ae935b38208e7a20..5066a39912b38df0e4cbf0fa58b3750f4513bff4 100644 --- a/src/net/http/internal/http2/flow.go +++ b/src/net/http/internal/http2/flow.go @@ -10,6 +10,9 @@ // inflowMinRefresh is the minimum number of bytes we'll send for a // flow control window update. const inflowMinRefresh = 4 << 10 +// maxFlowWindow is the maximum size of a flow control window. +const maxFlowWindow = (1 << 31) - 1 + // inflow accounts for an inbound flow control window. // It tracks both the latest window sent to the peer (used for enforcement) // and the accumulated unsent window. @@ -74,47 +77,99 @@ f2.avail -= int32(n) return true } -// outflow is the outbound flow control window's size. +// connOutflow is connection-level outbound flow control. +type connOutflow struct { + initial int32 // SETTINGS_INITIAL_WINDOW_SIZE, changes with settings updates + n int32 // connection-level flow control window + flowErr bool // set when a flow control error is encountered +} + +func (f *connOutflow) init() { + f.initial = initialWindowSize // initial stream window size + f.n = initialWindowSize // current connection window size +} + +func (f *connOutflow) changeInitialWindowSize(size int64) bool { + if size > maxFlowWindow { + f.flowErr = true + return false + } + f.initial = int32(size) + return true +} + +func (f *connOutflow) add(n int32) bool { + sum := int64(f.n) + int64(n) + if sum > maxFlowWindow { + f.flowErr = true + return false + } + f.n += n + return true +} + +// outflow is the stream-level outbound flow control window's size. type outflow struct { _ incomparable - // n is the number of DATA bytes we're allowed to send. - // An outflow is kept both on a conn and a per-stream. - n int32 + // delta is the difference between the stream's flow control window and + // the connection's initial window size (conn.initial). + // + // Another view is that delta is the number of flow control bytes provided to this + // stream in WINDOW_UPDATE frames, less the number of bytes sent on the stream. + delta int32 // conn points to the shared connection-level outflow that is - // shared by all streams on that conn. It is nil for the outflow - // that's on the conn directly. - conn *outflow + // shared by all streams on that conn. + conn *connOutflow } -func (f *outflow) setConnFlow(cf *outflow) { f.conn = cf } - -func (f *outflow) available() int32 { - n := f.n - if f.conn != nil && f.conn.n < n { - n = f.conn.n +func (f *outflow) available() (int32, bool) { + if f.conn == nil { + return maxFlowWindow, true // only happens in tests + } + if f.conn.flowErr { + // Block all sending once any stream observes a flow control error. + return 0, false } - return n + n := int64(f.conn.initial) + int64(f.delta) + if n > maxFlowWindow { + f.conn.flowErr = true + return 0, false + } + return min(int32(n), f.conn.n), true } func (f *outflow) take(n int32) { - if n > f.available() { + if f.conn == nil { + return // only happens in tests + } + avail, _ := f.available() + if n > avail { panic("internal error: took too much") } - f.n -= n - if f.conn != nil { - f.conn.n -= n - } + f.delta -= n + f.conn.n -= n } // add adds n bytes (positive or negative) to the flow control window. // It returns false if the sum would exceed 2^31-1. func (f *outflow) add(n int32) bool { - sum := f.n + n - if (sum > n) == (f.n > 0) { - f.n = sum - return true + if f.conn == nil { + return true // only happens in tests + } + avail := int64(f.conn.initial) + int64(f.delta) + if avail > maxFlowWindow { + // An earlier change to the initial window pushed this stream over the limit. + // This is a connection-level flow control error. + f.conn.flowErr = true + return false } - return false + if avail+int64(n) > maxFlowWindow { + // This update would push the stream over the limit. + // This is a stream-level flow control error. + return false + } + f.delta += n + return true } diff --git a/src/net/http/internal/http2/flow_test.go b/src/net/http/internal/http2/flow_test.go index cae4f38c0c426039f9aed38006b9153edef3fe6f..b4b0f47584a05c4b18cae41c60a63182cba4451b 100644 --- a/src/net/http/internal/http2/flow_test.go +++ b/src/net/http/internal/http2/flow_test.go @@ -60,43 +60,42 @@ } func TestOutFlow(t *testing.T) { var st outflow - var conn outflow + var conn connOutflow + st.conn = &conn st.add(3) conn.add(2) - if got, want := st.available(), int32(3); got != want { - t.Errorf("available = %d; want %d", got, want) - } - st.setConnFlow(&conn) - if got, want := st.available(), int32(2); got != want { - t.Errorf("after parent setup, available = %d; want %d", got, want) + if got, ok := st.available(); !ok || got != 2 { + t.Errorf("available = %d, %v; want 2, true", got, ok) } st.take(2) - if got, want := conn.available(), int32(0); got != want { + if got, want := conn.n, int32(0); got != want { t.Errorf("after taking 2, conn = %d; want %d", got, want) } - if got, want := st.available(), int32(0); got != want { - t.Errorf("after taking 2, stream = %d; want %d", got, want) + if got, ok := st.available(); !ok || got != 0 { + t.Errorf("after taking 2, stream = %d, %v; want 0, true", got, ok) } } func TestOutFlowAdd(t *testing.T) { var f outflow + f.conn = &connOutflow{} + f.conn.add(1<<31 - 1) if !f.add(1) { t.Fatal("failed to add 1") } if !f.add(-1) { t.Fatal("failed to add -1") } - if got, want := f.available(), int32(0); got != want { - t.Fatalf("size = %d; want %d", got, want) + if got, ok := f.available(); !ok || got != 0 { + t.Fatalf("size = %d, %v; want 0, true", got, ok) } if !f.add(1<<31 - 1) { t.Fatal("failed to add 2^31-1") } - if got, want := f.available(), int32(1<<31-1); got != want { - t.Fatalf("size = %d; want %d", got, want) + if got, ok := f.available(); !ok || got != 1<<31-1 { + t.Fatalf("size = %d, %v; want %d, true", got, ok, 1<<31-1) } if f.add(1) { t.Fatal("adding 1 to max shouldn't be allowed") @@ -105,6 +104,8 @@ } func TestOutFlowAddOverflow(t *testing.T) { var f outflow + f.conn = &connOutflow{} + f.conn.add(1<<31 - 1) if !f.add(0) { t.Fatal("failed to add 0") } @@ -126,14 +127,14 @@ } if !f.add(-3) { t.Fatal("failed to add -3") } - if got, want := f.available(), int32(-2); got != want { - t.Fatalf("size = %d; want %d", got, want) + if got, ok := f.available(); !ok || got != -2 { + t.Fatalf("size = %d, %v; want -2, true", got, ok) } if !f.add(1<<31 - 1) { t.Fatal("failed to add 2^31-1") } - if got, want := f.available(), int32(1+-3+(1<<31-1)); got != want { - t.Fatalf("size = %d; want %d", got, want) + if got, ok := f.available(); !ok || got != 1+-3+(1<<31-1) { + t.Fatalf("size = %d, %v; want %d, true", got, ok, 1+-3+(1<<31-1)) } } diff --git a/src/net/http/internal/http2/server.go b/src/net/http/internal/http2/server.go index c370685fe580fc3294393885efa34320a63fe37e..c1fb306977dfe8d154dfcea97fefc8738d989aec 100644 --- a/src/net/http/internal/http2/server.go +++ b/src/net/http/internal/http2/server.go @@ -275,7 +275,6 @@ bodyReadCh: make(chan bodyReadMsg), // buffering doesn't matter either way doneServing: make(chan struct{}), clientMaxStreams: math.MaxUint32, // Section 6.5.2: "Initially, there is no limit to this value" advMaxStreams: uint32(conf.MaxConcurrentStreams), - initialStreamSendWindowSize: initialWindowSize, initialStreamRecvWindowSize: int32(conf.MaxReceiveBufferPerStream), maxFrameSize: initialMaxFrameSize, pingTimeout: conf.PingTimeout, @@ -301,7 +300,7 @@ // These start at the RFC-specified defaults. If there is a higher // configured value for inflow, that will be updated when we send a // WINDOW_UPDATE shortly after sending SETTINGS. - sc.flow.add(initialWindowSize) + sc.flow.init() sc.inflow.init(initialWindowSize) sc.hpackEncoder = hpack.NewEncoder(&sc.headerWriteBuf) sc.hpackEncoder.SetMaxDynamicTableSizeLimit(uint32(conf.MaxEncoderHeaderTableSize)) @@ -418,7 +417,7 @@ wantWriteFrameCh chan FrameWriteRequest // from handlers -> serve wroteFrameCh chan frameWriteResult // from writeFrameAsync -> serve, tickles more frame writes bodyReadCh chan bodyReadMsg // from handlers -> serve serveMsgCh chan any // misc messages & code to send to / run on the serve loop - flow outflow // conn-wide (not stream-specific) outbound flow control + flow connOutflow // conn-wide (not stream-specific) outbound flow control inflow inflow // conn-wide inbound flow control tlsState *tls.ConnectionState // shared by all handlers, like net/http remoteAddrStr string @@ -442,7 +441,6 @@ maxClientStreamID uint32 // max ever seen from client (odd), or 0 if there have been no client requests maxPushPromiseID uint32 // ID of the last push promise (even), or 0 if there have been no pushes streams map[uint32]*stream unstartedHandlers []unstartedHandler - initialStreamSendWindowSize int32 initialStreamRecvWindowSize int32 maxFrameSize int32 peerMaxHeaderListSize uint32 // zero means unknown (default) @@ -1260,6 +1258,11 @@ return } sc.inFrameScheduleLoop = true for !sc.writingFrameAsync { + if sc.flow.flowErr && (!sc.inGoAway || sc.goAwayCode == ErrCodeNo) { + sc.inGoAway = true + sc.needToSendGoAway = true + sc.goAwayCode = ErrCodeFlowControl + } if sc.needToSendGoAway { sc.needToSendGoAway = false sc.startFrameWrite(FrameWriteRequest{ @@ -1281,6 +1284,9 @@ if wr.isControl() { sc.queuedControlFrames-- } sc.startFrameWrite(wr) + continue + } + if sc.flow.flowErr { continue } } @@ -1515,6 +1521,10 @@ // NOT treat this as an error, see Section 5.1." return nil } if !st.flow.add(int32(f.Increment)) { + if st.flow.conn.flowErr { + // This is a lazily-detected connection-level flow control error. + return sc.countError("bad_flow", ConnectionError(ErrCodeFlowControl)) + } return sc.countError("bad_flow", streamError(f.StreamID, ErrCodeFlowControl)) } default: // connection-level flow control @@ -1662,28 +1672,14 @@ } func (sc *serverConn) processSettingInitialWindowSize(val uint32) error { sc.serveG.check() - // Note: val already validated to be within range by - // processSetting's Valid call. - - // "A SETTINGS frame can alter the initial flow control window - // size for all current streams. When the value of - // SETTINGS_INITIAL_WINDOW_SIZE changes, a receiver MUST - // adjust the size of all stream flow control windows that it - // maintains by the difference between the new value and the - // old value." - old := sc.initialStreamSendWindowSize - sc.initialStreamSendWindowSize = int32(val) - growth := int32(val) - old // may be negative - for _, st := range sc.streams { - if !st.flow.add(growth) { - // 6.9.2 Initial Flow Control Window Size - // "An endpoint MUST treat a change to - // SETTINGS_INITIAL_WINDOW_SIZE that causes any flow - // control window to exceed the maximum size as a - // connection error (Section 5.4.1) of type - // FLOW_CONTROL_ERROR." - return sc.countError("setting_win_size", ConnectionError(ErrCodeFlowControl)) - } + if !sc.flow.changeInitialWindowSize(int64(val)) { + // 6.9.2 Initial Flow Control Window Size + // "An endpoint MUST treat a change to + // SETTINGS_INITIAL_WINDOW_SIZE that causes any flow + // control window to exceed the maximum size as a + // connection error (Section 5.4.1) of type + // FLOW_CONTROL_ERROR." + return sc.countError("setting_win_size", ConnectionError(ErrCodeFlowControl)) } return nil } @@ -2083,7 +2079,6 @@ cancelCtx: cancelCtx, } st.cw.Init() st.flow.conn = &sc.flow // link to conn-level counter - st.flow.add(sc.initialStreamSendWindowSize) st.inflow.init(sc.initialStreamRecvWindowSize) if writeTimeout := sc.hs.WriteTimeout(); writeTimeout > 0 { st.writeDeadline = time.AfterFunc(writeTimeout, st.onWriteTimeout) diff --git a/src/net/http/internal/http2/server_test.go b/src/net/http/internal/http2/server_test.go index a1fb00f92c0b8e537dceda03f47baed359c6d4ad..38ea7afac3ff70870beef3cdb219e28586d5229d 100644 --- a/src/net/http/internal/http2/server_test.go +++ b/src/net/http/internal/http2/server_test.go @@ -1436,6 +1436,9 @@ if err := st.fr.WriteWindowUpdate(1, 1<<31-1); err != nil { t.Fatal(err) } st.wantRSTStream(1, ErrCodeFlowControl) + // Connection is still alive, even if the stream has been reset. + st.writePing(false, [8]byte{}) + st.wantFrameType(FramePing) } // testServerPostUnblock sends a hanging POST with unsent data to handler, @@ -5094,6 +5097,9 @@ BlockFragment: st.encodeHeader(":method", "POST"), EndStream: false, // data coming EndHeaders: true, }) + call := st.nextHandlerCall() + http.NewResponseController(call.w).Flush() + st.wantFrameType(FrameHeaders) // Give this stream some additional flow control. const windowIncrease = 1000 @@ -5104,6 +5110,12 @@ // Adjust the initial flow control window. The stream is now over the limit. const maxWindowSize = (1 << 31) - 1 // RFC 9113, 6.9.1 const maxInitialWindowSize = maxWindowSize - windowIncrease st.writeSettings(Setting{SettingInitialWindowSize, maxInitialWindowSize + 1}) + st.wantSettingsAck() + + // We detect this condition lazily. Write something to the stream so we notice. + call.w.Write([]byte("hello")) + http.NewResponseController(call.w).Flush() + st.wantGoAway(1, ErrCodeFlowControl) } @@ -5122,6 +5134,9 @@ BlockFragment: st.encodeHeader(":method", "POST"), EndStream: false, // data coming EndHeaders: true, }) + call := st.nextHandlerCall() + http.NewResponseController(call.w).Flush() + st.wantFrameType(FrameHeaders) // Give this stream some additional flow control. const windowIncrease = 1000 @@ -5133,6 +5148,10 @@ const maxWindowSize = (1 << 31) - 1 // RFC 9113, 6.9.1 const maxInitialWindowSize = maxWindowSize - windowIncrease st.writeSettings(Setting{SettingInitialWindowSize, maxInitialWindowSize}) st.wantSettingsAck() + + call.w.Write([]byte("hello")) + http.NewResponseController(call.w).Flush() + st.wantFrameType(FrameData) st.wantIdle() } diff --git a/src/net/http/internal/http2/transport.go b/src/net/http/internal/http2/transport.go index bbf566fcc47edd8eaed49d40c0466cfff13d473b..6490019c51be00be5d1ca16dbbc6f92662be3fec 100644 --- a/src/net/http/internal/http2/transport.go +++ b/src/net/http/internal/http2/transport.go @@ -144,11 +144,11 @@ 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 outflow // our conn-level flow control quota (cs.outflow is per stream) - inflow inflow // peer's conn-level flow control - doNotReuse bool // whether conn is marked to not be reused for any future requests + mu sync.Mutex // guards following + cond *sync.Cond // hold mu; broadcast on flow/closed changes + flow connOutflow // our conn-level flow control quota (cs.outflow is per stream) + inflow inflow // peer's conn-level flow control + doNotReuse bool // whether conn is marked to not be reused for any future requests closing bool closed bool closedOnIdle bool // true if conn was closed for idleness @@ -170,7 +170,6 @@ maxFrameSize uint32 maxConcurrentStreams uint32 peerMaxHeaderListSize uint64 peerMaxHeaderTableSize uint32 - initialWindowSize uint32 initialStreamRecvWindowSize int32 readIdleTimeout time.Duration pingTimeout time.Duration @@ -619,7 +618,6 @@ tconn: c, readerDone: make(chan struct{}), nextStreamID: 1, maxFrameSize: 16 << 10, // spec default - initialWindowSize: 65535, // spec default initialStreamRecvWindowSize: int32(conf.MaxReceiveBufferPerStream), maxConcurrentStreams: initialMaxConcurrentStreams, // "infinite", per spec. Use a smaller value until we have received server settings. strictMaxConcurrentStreams: conf.StrictMaxConcurrentRequests, @@ -644,7 +642,7 @@ t.vlogf("http2: Transport creating client conn %p to %v", cc, c.RemoteAddr()) } cc.cond = sync.NewCond(&cc.mu) - cc.flow.add(int32(initialWindowSize)) + cc.flow.init() // TODO: adjust this writer size to account for frame size + // MTU + crypto/tls record padding. @@ -1039,6 +1037,33 @@ cc.mu.Unlock() cc.closeConn() } +func (cc *ClientConn) goAwayAndClose(code ErrCode) { + cc.mu.Lock() + closed := cc.closed + cc.closing = true + cc.closed = true + cc.mu.Unlock() + if closed { + return + } + if f := cc.fr.countError; f != nil { + f(fmt.Sprintf("conn_close_error_%s", code.stringToken())) + } + done := make(chan struct{}) + go func() { + defer close(done) + cc.wmu.Lock() + cc.fr.WriteGoAway(0, code, nil) + cc.bw.Flush() + cc.wmu.Unlock() + }() + select { + case <-done: + case <-time.After(250 * time.Millisecond): + } + cc.closeForError(fmt.Errorf("http2: closing connection with %v", code)) +} + // Close closes the client connection immediately. // // In-flight requests are interrupted. For a graceful shutdown, use Shutdown instead. @@ -1460,7 +1485,6 @@ } if bodyClosed != nil { <-bodyClosed } - if err != nil && cs.sentEndStream { // If the connection is closed immediately after the response is read, // we may be aborted before finishing up here. If the stream was closed @@ -1473,7 +1497,9 @@ } } if err != nil { cs.abortStream(err) // possibly redundant, but harmless - if cs.sentHeaders { + if ce, ok := err.(ConnectionError); ok { + cc.goAwayAndClose(ErrCode(ce)) + } else if cs.sentHeaders { if se, ok := err.(StreamError); ok { if se.Cause != errFromPeer { cc.writeStreamReset(cs.ID, se.Code, false, err) @@ -1799,8 +1825,12 @@ case <-cs.reqCancel: return 0, errRequestCanceled default: } - if a := cs.flow.available(); a > 0 { - take := a + avail, ok := cs.flow.available() + if !ok { + return 0, ConnectionError(ErrCodeFlowControl) + } + if avail > 0 { + take := avail if int(take) > maxBytes { take = int32(maxBytes) // can't truncate int; take is int32 @@ -1861,8 +1891,7 @@ } // requires cc.mu be held. func (cc *ClientConn) addStreamLocked(cs *clientStream) { - cs.flow.add(int32(cc.initialWindowSize)) - cs.flow.setConnFlow(&cc.flow) + cs.flow.conn = &cc.flow cs.inflow.init(cc.initialStreamRecvWindowSize) cs.ID = cc.nextStreamID cc.nextStreamID += 2 @@ -2614,6 +2643,10 @@ // If headerOrData is true, it clears rst.StreamPingsBlocked. func (rl *clientConnReadLoop) streamByID(id uint32, headerOrData bool) *clientStream { rl.cc.mu.Lock() defer rl.cc.mu.Unlock() + return rl.streamByIDLocked(id, headerOrData) +} + +func (rl *clientConnReadLoop) streamByIDLocked(id uint32, headerOrData bool) *clientStream { if headerOrData { // Work around an unfortunate gRPC behavior. // See comment on ClientConn.rstStreamPingsBlocked for details. @@ -2732,18 +2765,10 @@ seenMaxConcurrentStreams = true case SettingMaxHeaderListSize: cc.peerMaxHeaderListSize = uint64(s.Val) case SettingInitialWindowSize: - // Adjust flow control of currently-open - // frames by the difference of the old initial - // window size and this one. - delta := int32(s.Val) - int32(cc.initialWindowSize) - for _, cs := range cc.streams { - if !cs.flow.add(delta) { - return ConnectionError(ErrCodeFlowControl) - } + if !cc.flow.changeInitialWindowSize(int64(s.Val)) { + return ConnectionError(ErrCodeFlowControl) } cc.cond.Broadcast() - - cc.initialWindowSize = s.Val case SettingHeaderTableSize: cc.henc.SetMaxDynamicTableSize(s.Val) cc.peerMaxHeaderTableSize = s.Val @@ -2785,29 +2810,30 @@ } func (rl *clientConnReadLoop) processWindowUpdate(f *WindowUpdateFrame) error { cc := rl.cc - cs := rl.streamByID(f.StreamID, notHeaderOrDataFrame) - if f.StreamID != 0 && cs == nil { - return nil - } - cc.mu.Lock() defer cc.mu.Unlock() - - fl := &cc.flow - if cs != nil { - fl = &cs.flow - } - if !fl.add(int32(f.Increment)) { - // For stream, the sender sends RST_STREAM with an error code of FLOW_CONTROL_ERROR - if cs != nil { + if f.StreamID == 0 { + if !cc.flow.add(int32(f.Increment)) { + return ConnectionError(ErrCodeFlowControl) + } + } else { + cs := rl.streamByIDLocked(f.StreamID, notHeaderOrDataFrame) + if cs == nil { + return nil + } + if !cs.flow.add(int32(f.Increment)) { + if cs.flow.conn.flowErr { + // This is a lazily-detected connection-level flow control error. + return ConnectionError(ErrCodeFlowControl) + } + // For stream, the sender sends RST_STREAM with + // an error code of FLOW_CONTROL_ERROR. rl.endStreamErrorLocked(cs, StreamError{ StreamID: f.StreamID, Code: ErrCodeFlowControl, }) return nil } - - return ConnectionError(ErrCodeFlowControl) } cc.cond.Broadcast() return nil diff --git a/src/net/http/internal/http2/transport_test.go b/src/net/http/internal/http2/transport_test.go index 9972e4f5c2c39f8827d785e42a13f80ee9ab0bb2..a31ed853a3037faba6437b73871d85d0e5fab7d5 100644 --- a/src/net/http/internal/http2/transport_test.go +++ b/src/net/http/internal/http2/transport_test.go @@ -1749,7 +1749,8 @@ func testTransportSettingsFlowControlUpdateBeyondLimit(t *testing.T) { tc := newTestClientConn(t) tc.greet() - req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + body := tc.newRequestBody() + req, _ := http.NewRequest("GET", "https://dummy.tld/", body) rt := tc.roundTrip(req) tc.wantFrameType(FrameHeaders) @@ -1762,6 +1763,12 @@ // Adjust the initial flow control window. The stream is now over the limit. const maxWindowSize = (1 << 31) - 1 // RFC 9113, 6.9.1 const maxInitialWindowSize = maxWindowSize - windowIncrease tc.writeSettings(Setting{SettingInitialWindowSize, maxInitialWindowSize + 1}) + tc.wantSettingsAck() + + // We detect this condition lazily. Write something to the stream so we notice. + body.writeBytes(1) + body.closeWithError(io.EOF) + tc.wantGoAway(0, ErrCodeFlowControl) } @@ -1774,7 +1781,8 @@ func testTransportSettingsFlowControlUpdateWithinLimit(t *testing.T) { tc := newTestClientConn(t) tc.greet() - req, _ := http.NewRequest("GET", "https://dummy.tld/", nil) + body := tc.newRequestBody() + req, _ := http.NewRequest("GET", "https://dummy.tld/", body) rt := tc.roundTrip(req) tc.wantFrameType(FrameHeaders) @@ -1788,6 +1796,15 @@ const maxWindowSize = (1 << 31) - 1 // RFC 9113, 6.9.1 const maxInitialWindowSize = maxWindowSize - windowIncrease tc.writeSettings(Setting{SettingInitialWindowSize, maxInitialWindowSize}) tc.wantSettingsAck() + + body.writeBytes(1) + body.closeWithError(io.EOF) + tc.wantData(wantData{ + streamID: 1, + endStream: true, + size: 1, + multiple: true, + }) tc.wantIdle() } diff --git a/src/net/http/internal/http2/writesched.go b/src/net/http/internal/http2/writesched.go index 883c4ddc0d07e397f4172c684dc2cefa4b0c4dc3..b4a45368f0c3d075da8021bc229b264c9bd8dec3 100644 --- a/src/net/http/internal/http2/writesched.go +++ b/src/net/http/internal/http2/writesched.go @@ -115,7 +115,11 @@ return wr, empty, 1 } // Might need to split after applying limits. - allowed := min(n, wr.stream.flow.available()) + avail, ok := wr.stream.flow.available() + if !ok { + return empty, empty, 0 + } + allowed := min(n, avail) if wr.stream.sc.maxFrameSize < allowed { allowed = wr.stream.sc.maxFrameSize } diff --git a/src/net/http/internal/http2/writesched_test.go b/src/net/http/internal/http2/writesched_test.go index 6dbd7f0adc72035ccb7320dcf1e48eff04401abf..0eb156a3e7823a8dcf9c665d5290f735a4da66ce 100644 --- a/src/net/http/internal/http2/writesched_test.go +++ b/src/net/http/internal/http2/writesched_test.go @@ -76,6 +76,7 @@ st := &stream{ id: 1, sc: &serverConn{maxFrameSize: 16}, } + st.flow.conn = &connOutflow{} const size = 32 wr := FrameWriteRequest{&writeData{st.id, make([]byte, size), true}, st, make(chan error)} if got, want := wr.DataSize(), size; got != want { @@ -113,6 +114,8 @@ st := &stream{ id: 1, sc: &serverConn{maxFrameSize: 16}, } + st.flow.conn = &connOutflow{} + st.flow.conn.add(maxFlowWindow) // conn-level flow is large const size = 32 wr := FrameWriteRequest{&writeData{st.id, make([]byte, size), true}, st, make(chan error)} if got, want := wr.DataSize(), size; got != want {