http: expect 100 rework

Move all handling of HTTP's `Expect: 100-continue` feature into a client
reader. Add sending flag `KEEP_SEND_TIMED` that triggers transfer
sending on general events like a timer.

HTTP installs a `CURL_CR_PROTOCOL` reader when announcing `Expect:
100-continue`. That reader works as follows:

- on first invocation, records time, starts the `EXPIRE_100_TIMEOUT`
  timer, disables `KEEP_SEND`, enables `KEEP_SEND_TIMER` and returns 0,
  eos=FALSE like a paused upload.

- on subsequent invocation it checks if the timer has expired. If so, it
  enables `KEEP_SEND` and switches to passing through reads to the
  underlying readers.

Transfer handling's `readwrite()` will be invoked when a timer expires
(like `EXPIRE_100_TIMEOUT`) or when data from the server arrives. Seeing
`KEEP_SEND_TIMER`, it will try to upload more data, which triggers
reading from the client readers again. Which then may lead to a new
pausing or cause the upload to start.

Flags and timestamps connected to this have been moved from
`SingleRequest` into the reader's context.

Closes #13110
This commit is contained in:
Stefan Eissing 2024-03-11 17:23:15 +01:00 committed by Daniel Stenberg
parent 3d0fd382a2
commit 80a3b830cc
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
17 changed files with 301 additions and 189 deletions

View file

@ -46,6 +46,7 @@ class TestUpload:
env.make_data_file(indir=env.gen_dir, fname="data-63k", fsize=63*1024)
env.make_data_file(indir=env.gen_dir, fname="data-64k", fsize=64*1024)
env.make_data_file(indir=env.gen_dir, fname="data-100k", fsize=100*1024)
env.make_data_file(indir=env.gen_dir, fname="data-1m+", fsize=(1024*1024)+1)
env.make_data_file(indir=env.gen_dir, fname="data-10m", fsize=10*1024*1024)
httpd.clear_extra_configs()
httpd.reload()
@ -501,3 +502,29 @@ class TestUpload:
up_speed = r.stats[0]['speed_upload']
assert (speed_limit * 0.5) <= up_speed <= (speed_limit * 1.5), f'{r.stats[0]}'
# upload larger data, triggering "Expect: 100-continue" code paths
@pytest.mark.parametrize("proto", ['http/1.1'])
def test_07_60_upload_exp100(self, env: Env, httpd, nghttpx, repeat, proto):
fdata = os.path.join(env.gen_dir, 'data-1m+')
read_delay = 1
curl = CurlClient(env=env)
url = f'https://{env.authority_for(env.domain1, proto)}/curltest/put?id=[0-0]'\
f'&read_delay={read_delay}s'
r = curl.http_put(urls=[url], fdata=fdata, alpn_proto=proto, extra_args=[
'--expect100-timeout', f'{read_delay+1}'
])
r.check_stats(count=1, http_status=200, exitcode=0)
# upload larger data, triggering "Expect: 100-continue" code paths
@pytest.mark.parametrize("proto", ['http/1.1'])
def test_07_61_upload_exp100_timeout(self, env: Env, httpd, nghttpx, repeat, proto):
fdata = os.path.join(env.gen_dir, 'data-1m+')
read_delay = 2
curl = CurlClient(env=env)
url = f'https://{env.authority_for(env.domain1, proto)}/curltest/put?id=[0-0]'\
f'&read_delay={read_delay}s'
r = curl.http_put(urls=[url], fdata=fdata, alpn_proto=proto, extra_args=[
'--expect100-timeout', f'{read_delay-1}'
])
r.check_stats(count=1, http_status=200, exitcode=0)

View file

@ -425,7 +425,7 @@ static int curltest_put_handler(request_rec *r)
apr_off_t rbody_len = 0;
const char *s_rbody_len;
const char *request_id = "none";
apr_time_t chunk_delay = 0;
apr_time_t read_delay = 0, chunk_delay = 0;
apr_array_header_t *args = NULL;
long l;
int i;
@ -450,6 +450,12 @@ static int curltest_put_handler(request_rec *r)
request_id = val;
continue;
}
else if(!strcmp("read_delay", arg)) {
rv = duration_parse(&read_delay, val, "s");
if(APR_SUCCESS == rv) {
continue;
}
}
else if(!strcmp("chunk_delay", arg)) {
rv = duration_parse(&chunk_delay, val, "s");
if(APR_SUCCESS == rv) {
@ -478,6 +484,9 @@ static int curltest_put_handler(request_rec *r)
ct = apr_table_get(r->headers_in, "content-type");
ap_set_content_type(r, ct? ct : "text/plain");
if(read_delay) {
apr_sleep(read_delay);
}
bb = apr_brigade_create(r->pool, c->bucket_alloc);
/* copy any request body into the response */
if((rv = ap_setup_client_block(r, REQUEST_CHUNKED_DECHUNK))) goto cleanup;