diff --git a/lib/vtls/vtls.c b/lib/vtls/vtls.c
index e928ba5d07..f654a9749c 100644
--- a/lib/vtls/vtls.c
+++ b/lib/vtls/vtls.c
@@ -1715,32 +1715,17 @@ static ssize_t ssl_cf_recv(struct Curl_cfilter *cf,
{
struct cf_call_data save;
ssize_t nread;
- size_t ntotal = 0;
CF_DATA_SAVE(save, cf, data);
*err = CURLE_OK;
- /* Do receive until we fill the buffer somehwhat or EGAIN, error or EOF */
- while(!ntotal || (len - ntotal) > (4*1024)) {
- *err = CURLE_OK;
- nread = Curl_ssl->recv_plain(cf, data, buf + ntotal, len - ntotal, err);
- if(nread < 0) {
- if(*err == CURLE_AGAIN && ntotal > 0) {
- /* we EAGAINed after having reed data, return the success amount */
- *err = CURLE_OK;
- break;
- }
- /* we have a an error to report */
- goto out;
- }
- else if(nread == 0) {
- /* eof */
- break;
- }
- ntotal += (size_t)nread;
- DEBUGASSERT((size_t)ntotal <= len);
+ nread = Curl_ssl->recv_plain(cf, data, buf, len, err);
+ if(nread > 0) {
+ DEBUGASSERT((size_t)nread <= len);
+ }
+ else if(nread == 0) {
+ /* eof */
+ *err = CURLE_OK;
}
- nread = (ssize_t)ntotal;
-out:
CURL_TRC_CF(data, cf, "cf_recv(len=%zu) -> %zd, %d", len,
nread, *err);
CF_DATA_RESTORE(cf, save);
diff --git a/tests/http/test_05_errors.py b/tests/http/test_05_errors.py
index b59f3f177a..68e49f7362 100644
--- a/tests/http/test_05_errors.py
+++ b/tests/http/test_05_errors.py
@@ -108,3 +108,30 @@ class TestErrors:
r.check_response(http_status=200, count=1)
# check that we did a downgrade
assert r.stats[0]['http_version'] == '1.1', r.dump_logs()
+
+ # On the URL used here, Apache is doing an "unclean" TLS shutdown,
+ # meaning it sends no shutdown notice and just closes TCP.
+ # The HTTP response delivers a body without Content-Length. We expect:
+ # - http/1.0 to fail since it relies on a clean connection close to
+ # detect the end of the body
+ # - http/1.1 to work since it will used "chunked" transfer encoding
+ # and stop receiving when that signals the end
+ # - h2 to work since it will signal the end of the response before
+ # and not see the "unclean" close either
+ @pytest.mark.parametrize("proto", ['http/1.0', 'http/1.1', 'h2'])
+ def test_05_04_unclean_tls_shutdown(self, env: Env, httpd, nghttpx, repeat, proto):
+ if proto == 'h3' and not env.have_h3():
+ pytest.skip("h3 not supported")
+ count = 10 if proto == 'h2' else 1
+ curl = CurlClient(env=env)
+ url = f'https://{env.authority_for(env.domain1, proto)}'\
+ f'/curltest/shutdown_unclean?id=[0-{count-1}]&chunks=4'
+ r = curl.http_download(urls=[url], alpn_proto=proto, extra_args=[
+ '--parallel',
+ ])
+ if proto == 'http/1.0':
+ r.check_exit_code(56)
+ else:
+ r.check_exit_code(0)
+ r.check_response(http_status=200, count=count)
+
diff --git a/tests/http/testenv/httpd.py b/tests/http/testenv/httpd.py
index 79497c5b30..c04c22699a 100644
--- a/tests/http/testenv/httpd.py
+++ b/tests/http/testenv/httpd.py
@@ -47,7 +47,7 @@ class Httpd:
'authn_core', 'authn_file',
'authz_user', 'authz_core', 'authz_host',
'auth_basic', 'auth_digest',
- 'alias', 'env', 'filter', 'headers', 'mime',
+ 'alias', 'env', 'filter', 'headers', 'mime', 'setenvif',
'socache_shmcb',
'rewrite', 'http2', 'ssl', 'proxy', 'proxy_http', 'proxy_connect',
'mpm_event',
@@ -389,6 +389,11 @@ class Httpd:
f' ',
f' SetHandler curltest-1_1-required',
f' ',
+ f' ',
+ f' SetHandler curltest-tweak',
+ f' SetEnv force-response-1.0 1',
+ f' ',
+ f' SetEnvIf Request_URI "/shutdown_unclean" ssl-unclean=1',
])
if self._auth_digest:
lines.extend([
diff --git a/tests/http/testenv/mod_curltest/mod_curltest.c b/tests/http/testenv/mod_curltest/mod_curltest.c
index ff1983d17f..a066be5222 100644
--- a/tests/http/testenv/mod_curltest/mod_curltest.c
+++ b/tests/http/testenv/mod_curltest/mod_curltest.c
@@ -347,7 +347,7 @@ static int curltest_tweak_handler(request_rec *r)
"request, %s", r->args? r->args : "(no args)");
r->status = http_status;
r->clength = -1;
- r->chunked = 1;
+ r->chunked = (r->proto_num >= HTTP_VERSION(1,1));
apr_table_setn(r->headers_out, "request-id", request_id);
apr_table_unset(r->headers_out, "Content-Length");
/* Discourage content-encodings */