]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
http2: when uploading data from stdin, fix eos forwarding
authorStefan Eissing <stefan@eissing.org>
Thu, 12 Sep 2024 08:03:33 +0000 (10:03 +0200)
committerDaniel Stenberg <daniel@haxx.se>
Thu, 12 Sep 2024 11:46:38 +0000 (13:46 +0200)
When uploading data from stdin ('-T -'), and the EOS was only detected
on a 0-length read, the EOS was not forwarded to the filters. This led
HTTP/2 to hang on not forwarding this to the server.

Added test_07_14 to reproduce and verify.

Fixes #14870
Reported-by: nekopsykose on github
Closes #14877

lib/http2.c
lib/request.c
lib/request.h
lib/transfer.c
tests/http/test_07_upload.py

index bc50a2f7a7fe4390206e8925f9a838d3afcbb736..df3e6f0df38fbf45b8c1114d81028aa6279c47d1 100644 (file)
@@ -1679,12 +1679,11 @@ static ssize_t req_body_read_callback(nghttp2_session *session,
   CURL_TRC_CF(data_s, cf, "[%d] req_body_read(len=%zu) eos=%d -> %zd, %d",
               stream_id, length, stream->body_eos, nread, result);
 
-  if(nread == 0)
-    return NGHTTP2_ERR_DEFERRED;
-  if(stream->body_eos && Curl_bufq_is_empty(&stream->sendbuf))
+  if(stream->body_eos && Curl_bufq_is_empty(&stream->sendbuf)) {
     *data_flags = NGHTTP2_DATA_FLAG_EOF;
-
-  return nread;
+    return nread;
+  }
+  return (nread == 0)? NGHTTP2_ERR_DEFERRED : nread;
 }
 
 #if !defined(CURL_DISABLE_VERBOSE_STRINGS)
index fb75e5577cdbb29e95f48fd98243efde3ce8f64d..6b2784c3ff08e6d62585ccab0adb305adbe4f8fd 100644 (file)
@@ -214,15 +214,19 @@ static CURLcode xfer_send(struct Curl_easy *data,
     eos = TRUE;
   }
   result = Curl_xfer_send(data, buf, blen, eos, pnwritten);
-  if(!result && *pnwritten) {
-    if(hds_len)
-      Curl_debug(data, CURLINFO_HEADER_OUT, (char *)buf,
-                 CURLMIN(hds_len, *pnwritten));
-    if(*pnwritten > hds_len) {
-      size_t body_len = *pnwritten - hds_len;
-      Curl_debug(data, CURLINFO_DATA_OUT, (char *)buf + hds_len, body_len);
-      data->req.writebytecount += body_len;
-      Curl_pgrsSetUploadCounter(data, data->req.writebytecount);
+  if(!result) {
+    if(eos && (blen == *pnwritten))
+      data->req.eos_sent = TRUE;
+    if(*pnwritten) {
+      if(hds_len)
+        Curl_debug(data, CURLINFO_HEADER_OUT, (char *)buf,
+                   CURLMIN(hds_len, *pnwritten));
+      if(*pnwritten > hds_len) {
+        size_t body_len = *pnwritten - hds_len;
+        Curl_debug(data, CURLINFO_DATA_OUT, (char *)buf + hds_len, body_len);
+        data->req.writebytecount += body_len;
+        Curl_pgrsSetUploadCounter(data, data->req.writebytecount);
+      }
     }
   }
   return result;
@@ -304,8 +308,17 @@ static CURLcode req_flush(struct Curl_easy *data)
     return Curl_xfer_flush(data);
   }
 
-  if(!data->req.upload_done && data->req.eos_read &&
-     Curl_bufq_is_empty(&data->req.sendbuf)) {
+  if(data->req.eos_read && !data->req.eos_sent) {
+    char tmp;
+    size_t nwritten;
+    result = xfer_send(data, &tmp, 0, 0, &nwritten);
+    if(result)
+      return result;
+    DEBUGASSERT(data->req.eos_sent);
+  }
+
+  if(!data->req.upload_done && data->req.eos_read && data->req.eos_sent) {
+    DEBUGASSERT(Curl_bufq_is_empty(&data->req.sendbuf));
     if(data->req.shutdown) {
       bool done;
       result = Curl_xfer_send_shutdown(data, &done);
index fb3f9f116815cd6d5ff42cbb523bf070a2ae0e54..c53c3eb5ae7e7f4787f93e21762f92581fb1e47c 100644 (file)
@@ -130,6 +130,7 @@ struct SingleRequest {
   BIT(download_done); /* set to TRUE when download is complete */
   BIT(eos_written);   /* iff EOS has been written to client */
   BIT(eos_read);      /* iff EOS has been read from the client */
+  BIT(eos_sent);      /* iff EOS has been sent to the server */
   BIT(rewind_read);   /* iff reader needs rewind at next start */
   BIT(upload_done);   /* set to TRUE when all request data has been sent */
   BIT(upload_aborted); /* set to TRUE when upload was aborted. Will also
index 0f42b3f2b4ddd600d17efacf168d1e1ed82d419d..ab8fd724314e4eaa96dc0faad8a79d31209b668a 100644 (file)
@@ -1253,8 +1253,8 @@ CURLcode Curl_xfer_send(struct Curl_easy *data,
   else if(!result && *pnwritten)
     data->info.request_size += *pnwritten;
 
-  DEBUGF(infof(data, "Curl_xfer_send(len=%zu) -> %d, %zu",
-               blen, result, *pnwritten));
+  DEBUGF(infof(data, "Curl_xfer_send(len=%zu, eos=%d) -> %d, %zu",
+               blen, eos, result, *pnwritten));
   return result;
 }
 
index 8dc06d45dafaa7d4de7f210d80a7abe9bf172103..70cc5918d6209ed8ac4860ceb33ad8723ca9117f 100644 (file)
@@ -155,6 +155,25 @@ class TestUpload:
             respdata = open(curl.response_file(i)).readlines()
             assert respdata == indata
 
+    # upload from stdin, issue #14870
+    @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
+    @pytest.mark.parametrize("indata", [
+        '', '1', '123\n456andsomething\n\n'
+    ])
+    def test_07_14_upload_stdin(self, env: Env, httpd, nghttpx, proto, indata):
+        if proto == 'h3' and not env.have_h3():
+            pytest.skip("h3 not supported")
+        if proto == 'h3' and env.curl_uses_lib('msh3'):
+            pytest.skip("msh3 stalls here")
+        count = 1
+        curl = CurlClient(env=env)
+        url = f'https://{env.authority_for(env.domain1, proto)}/curltest/put?id=[0-{count-1}]'
+        r = curl.http_put(urls=[url], data=indata, alpn_proto=proto)
+        r.check_stats(count=count, http_status=200, exitcode=0)
+        for i in range(count):
+            respdata = open(curl.response_file(i)).readlines()
+            assert respdata == [f'{len(indata)}']
+
     # upload data parallel, check that they were echoed
     @pytest.mark.parametrize("proto", ['h2', 'h3'])
     def test_07_20_upload_parallel(self, env: Env, httpd, nghttpx, repeat, proto):
@@ -581,3 +600,21 @@ class TestUpload:
             '--expect100-timeout', f'{read_delay-1}'
         ])
         r.check_stats(count=1, http_status=200, exitcode=0)
+
+    # speed limited on echo handler
+    @pytest.mark.parametrize("proto", ['http/1.1', 'h2', 'h3'])
+    def test_07_51_echo_speed_limit(self, env: Env, httpd, nghttpx, proto, repeat):
+        if proto == 'h3' and not env.have_h3():
+            pytest.skip("h3 not supported")
+        count = 1
+        fdata = os.path.join(env.gen_dir, 'data-100k')
+        speed_limit = 50 * 1024
+        curl = CurlClient(env=env)
+        url = f'https://{env.authority_for(env.domain1, proto)}/curltest/echo?id=[0-0]'
+        r = curl.http_upload(urls=[url], data=f'@{fdata}', alpn_proto=proto,
+                             with_headers=True, extra_args=[
+            '--limit-rate', f'{speed_limit}'
+        ])
+        r.check_response(count=count, http_status=200)
+        up_speed = r.stats[0]['speed_upload']
+        assert (speed_limit * 0.5) <= up_speed <= (speed_limit * 1.5), f'{r.stats[0]}'