Skip to content

Commit e492c7c

Browse files
icingbagder
authored andcommitted
transfer: fix upload rate limiting, add test cases
- add test cases for rate limiting uploads for all http versions - fix transfer loop handling of limits. Signal a re-receive attempt only on exhausting maxloops without an EAGAIN - fix `data->state.selectbits` forcing re-receive to also set re-sending when transfer is doing this. Reported-by: Karthikdasari0423 on github Fixes curl#12559 Closes curl#12586
1 parent 8b1d229 commit e492c7c

File tree

3 files changed

+60
-6
lines changed

3 files changed

+60
-6
lines changed

lib/transfer.c

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ static CURLcode readwrite_data(struct Curl_easy *data,
428428
size_t blen;
429429
size_t consumed;
430430
int maxloops = 10;
431-
curl_off_t max_recv = data->set.max_recv_speed ? 0 : CURL_OFF_T_MAX;
431+
curl_off_t total_received = 0;
432432
bool data_eof_handled = FALSE;
433433

434434
DEBUGASSERT(data->state.buffer);
@@ -439,6 +439,7 @@ static CURLcode readwrite_data(struct Curl_easy *data,
439439
do {
440440
bool is_empty_data = FALSE;
441441
size_t bytestoread = data->set.buffer_size;
442+
442443
/* For HTTP/2 and HTTP/3, read data without caring about the content
443444
length. This is safe because body in HTTP/2 is always segmented
444445
thanks to its framing layer. Meanwhile, we have to call Curl_read
@@ -447,6 +448,15 @@ static CURLcode readwrite_data(struct Curl_easy *data,
447448
bool is_http3 = Curl_conn_is_http3(data, conn, FIRSTSOCKET);
448449
data_eof_handled = is_http3 || Curl_conn_is_http2(data, conn, FIRSTSOCKET);
449450

451+
if(data->set.max_recv_speed) {
452+
/* Limit the amount we read here, break on reaching it */
453+
curl_off_t net_limit = data->set.max_recv_speed - total_received;
454+
if(net_limit <= 0)
455+
break;
456+
if((size_t)net_limit < bytestoread)
457+
bytestoread = (size_t)net_limit;
458+
}
459+
450460
/* Each loop iteration starts with a fresh buffer and handles
451461
* all data read into it. */
452462
buf = data->state.buffer;
@@ -654,7 +664,7 @@ static CURLcode readwrite_data(struct Curl_easy *data,
654664
}
655665
#endif /* CURL_DISABLE_HTTP */
656666

657-
max_recv -= blen;
667+
total_received += blen;
658668

659669
if(!k->chunk && (blen || k->badheader || is_empty_data)) {
660670
/* If this is chunky transfer, it was already written */
@@ -712,11 +722,13 @@ static CURLcode readwrite_data(struct Curl_easy *data,
712722
break;
713723
}
714724

715-
} while((max_recv > 0) && data_pending(data) && maxloops--);
725+
} while(maxloops-- && data_pending(data));
716726

717-
if(maxloops <= 0 || max_recv <= 0) {
718-
/* we mark it as read-again-please */
727+
if(maxloops <= 0) {
728+
/* did not read until EAGAIN, mark read-again-please */
719729
data->state.select_bits = CURL_CSELECT_IN;
730+
if((k->keepon & KEEP_SENDBITS) == KEEP_SEND)
731+
data->state.select_bits |= CURL_CSELECT_OUT;
720732
}
721733

722734
if(((k->keepon & (KEEP_RECV|KEEP_SEND)) == KEEP_SEND) &&

tests/http/test_07_upload.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,3 +461,42 @@ def check_download(self, count, srcfile, curl):
461461
tofile=dfile,
462462
n=1))
463463
assert False, f'download {dfile} differs:\n{diff}'
464+
465+
# speed limited on put handler
466+
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
467+
def test_07_50_put_speed_limit(self, env: Env, httpd, nghttpx, proto, repeat):
468+
if proto == 'h3' and not env.have_h3():
469+
pytest.skip("h3 not supported")
470+
count = 1
471+
fdata = os.path.join(env.gen_dir, 'data-100k')
472+
up_len = 100 * 1024
473+
speed_limit = 20 * 1024
474+
curl = CurlClient(env=env)
475+
url = f'https://{env.authority_for(env.domain1, proto)}/curltest/put?id=[0-0]'
476+
r = curl.http_put(urls=[url], fdata=fdata, alpn_proto=proto,
477+
with_headers=True, extra_args=[
478+
'--limit-rate', f'{speed_limit}'
479+
])
480+
r.check_response(count=count, http_status=200)
481+
assert r.responses[0]['header']['received-length'] == f'{up_len}', f'{r.responses[0]}'
482+
up_speed = r.stats[0]['speed_upload']
483+
assert (speed_limit * 0.5) <= up_speed <= (speed_limit * 1.5), f'{r.stats[0]}'
484+
485+
# speed limited on echo handler
486+
@pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
487+
def test_07_51_echo_speed_limit(self, env: Env, httpd, nghttpx, proto, repeat):
488+
if proto == 'h3' and not env.have_h3():
489+
pytest.skip("h3 not supported")
490+
count = 1
491+
fdata = os.path.join(env.gen_dir, 'data-100k')
492+
speed_limit = 20 * 1024
493+
curl = CurlClient(env=env)
494+
url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo?id=[0-0]'
495+
r = curl.http_upload(urls=[url], data=f'@{fdata}', alpn_proto=proto,
496+
with_headers=True, extra_args=[
497+
'--limit-rate', f'{speed_limit}'
498+
])
499+
r.check_response(count=count, http_status=200)
500+
up_speed = r.stats[0]['speed_upload']
501+
assert (speed_limit * 0.5) <= up_speed <= (speed_limit * 1.5), f'{r.stats[0]}'
502+

tests/http/testenv/mod_curltest/mod_curltest.c

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ static int curltest_put_handler(request_rec *r)
423423
char buffer[16*1024];
424424
const char *ct;
425425
apr_off_t rbody_len = 0;
426+
const char *s_rbody_len;
426427
const char *request_id = "none";
427428
apr_time_t chunk_delay = 0;
428429
apr_array_header_t *args = NULL;
@@ -491,7 +492,9 @@ static int curltest_put_handler(request_rec *r)
491492
}
492493
}
493494
/* we are done */
494-
rv = apr_brigade_printf(bb, NULL, NULL, "%"APR_OFF_T_FMT, rbody_len);
495+
s_rbody_len = apr_psprintf(r->pool, "%"APR_OFF_T_FMT, rbody_len);
496+
apr_table_setn(r->headers_out, "Received-Length", s_rbody_len);
497+
rv = apr_brigade_puts(bb, NULL, NULL, s_rbody_len);
495498
if(APR_SUCCESS != rv) goto cleanup;
496499
b = apr_bucket_eos_create(c->bucket_alloc);
497500
APR_BRIGADE_INSERT_TAIL(bb, b);

0 commit comments

Comments
 (0)