Skip to content

Implicit RST_STREAM blocked until buffered DATA is fully drained #880

@mmishra100

Description

@mmishra100

When both ResponseFuture and SendStream handles are dropped for a stream that has buffered DATA frames, maybe_cancel() schedules an implicit reset via schedule_implicit_reset(CANCEL). However, the resulting RST_STREAM is not sent until all buffered DATA frames have been drained through flow control first. If the stream's send window is exhausted and the server never sends a WINDOW_UPDATE, the DATA never drains and the RST_STREAM is indefinitely deferred.

How this happens

schedule_implicit_reset() cannot call clear_queue() because it is invoked from type-erased OpaqueStreamRef::drop() which has no access to Buffer<Frame<B>>. It sets ScheduledLibraryReset on the stream state and defers to pop_frame().

In pop_frame(), the ScheduledLibraryReset state is only checked after the stream's pending_send queue is fully drained (the None arm). Before that, pop_frame() attempts to send each buffered DATA frame through flow control like any normal frame. The implicit RST_STREAM effectively sits behind all buffered DATA in priority, rather than preempting it.

This contrasts with explicit send_reset() which works correctly — it calls clear_queue() to discard buffered DATA, queues RST_STREAM directly, and reclaims capacity immediately.

Reproduction

Steps:

  1. Set up a server that advertises initial_window_size = 0 (or any small value that will be exhausted)
  2. Client sends a POST request with end_of_stream = false
  3. Client buffers DATA on the stream via send_data()
  4. Client drops both ResponseFuture and SendStream
  5. Observe that RST_STREAM is never sent — it is deferred behind the buffered DATA which cannot drain through the exhausted window

Test (for tests/h2-tests/tests/flow_control.rs):

/// When both handles of a stream are dropped while DATA frames are
/// buffered and the send window is exhausted, the implicit RST_STREAM
/// should be sent promptly.
///
/// Currently it is deferred until the buffered DATA is fully drained
/// through flow control, which never completes when the window is zero.
#[tokio::test]
async fn implicit_rst_stream_with_buffered_data_and_zero_window() {
    h2_support::trace_init!();

    let test = async {
        let (io, mut srv) = mock::new();

        let srv = async move {
            let settings = srv
                .assert_client_handshake_with_settings(
                    frames::settings().initial_window_size(0),
                )
                .await;
            assert_default_settings!(settings);

            srv.recv_frame(
                frames::headers(1)
                    .request("POST", "https://example.com/"),
            )
            .await;

            srv.send_frame(frames::headers(1).response(200)).await;

            srv.recv_frame(frames::reset(1).cancel()).await;
        };

        let h2 = async move {
            let (mut client, mut h2) = client::handshake(io).await.unwrap();

            let request = Request::builder()
                .method(Method::POST)
                .uri("https://example.com/")
                .body(())
                .unwrap();

            let (response, mut send_stream) =
                client.send_request(request, false).unwrap();

            let response = h2.drive(response).await.unwrap();
            assert_eq!(response.status(), StatusCode::OK);

            // Buffer DATA that can never be sent (window is 0).
            send_stream
                .send_data(vec![0u8; 10].into(), false)
                .unwrap();

            // Drop both handles — triggers schedule_implicit_reset(CANCEL).
            drop(response);
            drop(send_stream);

            h2.await.unwrap();
        };

        join(srv, h2).await;
    };

    let result = tokio::time::timeout(
        Duration::from_secs(60), test
    ).await;
    assert!(
        result.is_ok(),
        "Timed out: implicit RST_STREAM was not sent \
         (deferred behind buffered DATA that could not drain)",
    );
}

Suggested fix

In pop_frame() in prioritize.rs, when a DATA frame is popped for a stream with get_scheduled_reset(), clear the queue and produce RST_STREAM immediately instead of trying to drain the DATA first:

Some(Frame::Data(frame)) => {
    if let Some(reason) = stream.state.get_scheduled_reset() {
        stream.pending_send.push_front(buffer, frame.into());
        self.clear_queue(buffer, &mut stream);
        self.reclaim_all_capacity(&mut stream, counts);
        stream.set_reset(reason, Initiator::Library);
        let frame = frame::Reset::new(stream.id, reason);
        Frame::Reset(frame)
    } else {
        // existing DATA flow control path
    }
}

This mirrors what explicit send_reset() already does, but triggered from within pop_frame() where Buffer<Frame<B>> is available.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions