int status_code; /* HTTP response status code */
uint32_t error; /* stream error code */
+ uint32_t local_window_size; /* the local recv window size */
bool closed; /* TRUE on stream close */
bool reset; /* TRUE on stream reset */
bool close_handled; /* TRUE if stream closure is handled by libcurl */
stream->closed = FALSE;
stream->close_handled = FALSE;
stream->error = NGHTTP2_NO_ERROR;
+ stream->local_window_size = H2_STREAM_WINDOW_SIZE;
stream->upload_left = 0;
H2_STREAM_LCTX(data) = stream;
struct stream_ctx *stream = H2_STREAM_CTX(data);
int32_t stream_id = frame->hd.stream_id;
CURLcode result;
+ size_t rbuflen;
int rv;
if(!stream) {
switch(frame->hd.type) {
case NGHTTP2_DATA:
+ rbuflen = Curl_bufq_len(&stream->recvbuf);
DEBUGF(LOG_CF(data, cf, "[h2sid=%d] FRAME[DATA len=%zu pad=%zu], "
"buffered=%zu, window=%d/%d",
- stream_id, frame->hd.length, frame->data.padlen,
- Curl_bufq_len(&stream->recvbuf),
+ stream_id, frame->hd.length, frame->data.padlen, rbuflen,
nghttp2_session_get_stream_effective_recv_data_length(
ctx->h2, stream->id),
nghttp2_session_get_stream_effective_local_window_size(
if(frame->hd.flags & NGHTTP2_FLAG_END_STREAM) {
drain_stream(cf, data, stream);
}
+ else if(rbuflen > stream->local_window_size) {
+ int32_t wsize = nghttp2_session_get_stream_local_window_size(
+ ctx->h2, stream->id);
+ if(wsize > 0 && (uint32_t)wsize != stream->local_window_size) {
+ /* H2 flow control is not absolute, as the server might not have the
+ * same view, yet. When we recieve more than we want, we enforce
+ * the local window size again to make nghttp2 send WINDOW_UPATEs
+ * accordingly. */
+ nghttp2_session_set_local_window_size(ctx->h2,
+ NGHTTP2_FLAG_NONE,
+ stream->id,
+ stream->local_window_size);
+ }
+ }
break;
case NGHTTP2_HEADERS:
DEBUGF(LOG_CF(data, cf, "[h2sid=%d] FRAME[HEADERS]", stream_id));
infof(data, "Using Stream ID: %u (easy handle %p)",
stream_id, (void *)data);
stream->id = stream_id;
+ stream->local_window_size = H2_STREAM_WINDOW_SIZE;
+ if(data->set.max_recv_speed) {
+ /* We are asked to only receive `max_recv_speed` bytes per second.
+ * Let's limit our stream window size around that, otherwise the server
+ * will send in large bursts only. We make the window 50% larger to
+ * allow for data in flight and avoid stalling. */
+ size_t n = (((data->set.max_recv_speed - 1) / H2_CHUNK_SIZE) + 1);
+ n += CURLMAX((n/2), 1);
+ if(n < (H2_STREAM_WINDOW_SIZE / H2_CHUNK_SIZE) &&
+ n < (UINT_MAX / H2_CHUNK_SIZE)) {
+ stream->local_window_size = (uint32_t)n * H2_CHUNK_SIZE;
+ }
+ }
out:
DEBUGF(LOG_CF(data, cf, "[h2sid=%d] submit -> %zd, %d",
}
goto out;
}
+
}
out:
DEBUGASSERT(data);
if(ctx && ctx->h2 && stream) {
- uint32_t window = !pause * H2_STREAM_WINDOW_SIZE;
+ uint32_t window = pause? 0 : stream->local_window_size;
CURLcode result;
int rv = nghttp2_session_set_local_window_size(ctx->h2,
size_t excess = 0; /* excess bytes read */
bool readmore = FALSE; /* used by RTP to signal for more data */
int maxloops = 100;
+ curl_off_t max_recv = data->set.max_recv_speed?
+ data->set.max_recv_speed : CURL_OFF_T_MAX;
char *buf = data->state.buffer;
DEBUGASSERT(buf);
}
k->bytecount += nread;
+ max_recv -= nread;
Curl_pgrsSetDownloadCounter(data, k->bytecount);
break;
}
- } while(data_pending(data) && maxloops--);
+ } while((max_recv > 0) && data_pending(data) && maxloops--);
- if(maxloops <= 0) {
+ if(maxloops <= 0 || max_recv <= 0) {
/* we mark it as read-again-please */
data->state.dselect_bits = CURL_CSELECT_IN;
*comeback = TRUE;
import filecmp
import logging
import os
+from datetime import timedelta
import pytest
from testenv import Env, CurlClient, LocalClient
# downloads should be there, but not necessarily complete
self.check_downloads(client, srcfile, count, complete=False)
+ # speed limited download
+ @pytest.mark.parametrize("proto", ['h2', 'h3'])
+ def test_02_24_speed_limit(self, env: Env, httpd, nghttpx, proto, repeat):
+ if proto == 'h3' and not env.have_h3():
+ pytest.skip("h3 not supported")
+ count = 1
+ url = f'https://{env.authority_for(env.domain1, proto)}/data-1m'
+ curl = CurlClient(env=env)
+ r = curl.http_download(urls=[url], alpn_proto=proto, extra_args=[
+ '--limit-rate', f'{196 * 1024}'
+ ])
+ r.check_response(count=count, http_status=200)
+ assert r.duration > timedelta(seconds=4), \
+ f'rate limited transfer should take more than 4s, not {r.duration}'
+
def check_downloads(self, client, srcfile: str, count: int,
complete: bool = True):
for i in range(count):