From 895262508c6bd452f2a26250dba5079229df445f Mon Sep 17 00:00:00 2001 From: "mohammad.alam" Date: Wed, 21 Oct 2026 12:42:22 -0400 Subject: [PATCH] conncache: poll an idle connection for pending input on every reuse Backport of upstream ef6b17cf12 (curl/curl#23412), adapted to 8.22.0: upstream's cpool_conn_can_be_closed() is spelled out as !CONN_INUSE(conn) && !conn->bits.connect_only. Since 2f1e94eaed the alive check is skipped when it ran less than a second ago, which also skipped the pending input check. An idle HTTP/1.1 connection closed by the server was then reused within that second; the request failed, curl retried on a fresh connection and had to rewind the upload, which fails for a paused, non-seekable streaming upload (CURLE_SEND_FAIL_REWIND). Poll for pending input on every reuse of an idle non-multiplexed connection, as 8.20 did. (cherry picked from commit ef6b17cf12d4af4dfb2af4f9bc35861772be71c7) --- lib/conncache.c | 13 ++++++-- tests/data/Makefile.am | 1 + tests/data/test1701 | 75 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 tests/data/test1701 diff --git a/lib/conncache.c b/lib/conncache.c index 01681d4ebbc9..4ee276246f0c 100644 --- a/lib/conncache.c +++ b/lib/conncache.c @@ -947,11 +947,19 @@ bool Curl_cpool_conn_seems_healthy(struct connectdata *conn, { struct Curl_easy *admin; bool healthy = TRUE; + bool poll_input; DEBUGASSERT(!data->conn); if(!CONN_INUSE(conn) && cpool_conn_maxage(data, conn, pnow)) /* too old? */ return FALSE; - if(curlx_ptimediff_ms(pnow, &conn->lastchecked) < 1000) + + /* An idle, non-multiplexed connection must not be reused when it has + * input pending (RFC 9112, section 9.2), so that is polled on every + * reuse. The once a second limit only applies to the other checks. */ + poll_input = !CONN_INUSE(conn) && !conn->bits.connect_only && + !Curl_conn_is_multiplex(conn, FIRSTSOCKET) && + !conn->scheme->run->connection_is_dead; + if(!poll_input && curlx_ptimediff_ms(pnow, &conn->lastchecked) < 1000) return TRUE; admin = Curl_get_admin(data); @@ -966,8 +974,7 @@ bool Curl_cpool_conn_seems_healthy(struct connectdata *conn, Curl_attach_connection(admin, conn, FALSE); healthy = Curl_conn_is_alive(admin, conn, &input_pending); Curl_detach_connection(admin); - if(healthy && input_pending && - !CONN_INUSE(conn) && !Curl_conn_is_multiplex(conn, FIRSTSOCKET)) { + if(healthy && input_pending && poll_input) { /* Non-multiplexed connections without attached transfers should * not have input pending. The input might be a TLS Notify Close, * for all we know. */ diff --git a/tests/data/Makefile.am b/tests/data/Makefile.am index 3b32fce7bb30..2266010d868d 100644 --- a/tests/data/Makefile.am +++ b/tests/data/Makefile.am @@ -1709,6 +1709,7 @@ TESTCASES = \ test1686 \ \ test1700 \ + test1701 \ test1702 \ test1703 \ test1704 \ diff --git a/tests/data/test1701 b/tests/data/test1701 new file mode 100644 index 000000000000..e4e3d044302a --- /dev/null +++ b/tests/data/test1701 @@ -0,0 +1,75 @@ + + + + +HTTP +HTTP GET +DELAY + + +# Server-side + +# the first response is exactly 60 bytes so that with writedelay the server +# sends it in three 20 byte writes and the unsolicited second response only +# starts arriving after the first transfer is complete and the connection +# has been returned to the pool + +HTTP/1.1 200 OK +Funny-head: yesyes +Content-Length: 6 + +first +HTTP/1.1 200 OK +Content-Length: 6 + +stray + + +HTTP/1.1 200 OK +Content-Length: 7 + +second + + +writedelay: 150 + + + +# Client-side + + +http + + +HTTP/1.1 idle connection with unsolicited data is not reused + +# --rate 1/s starts the second request 1000 ms after the first one started, +# which is about 700 ms after the first response completed and the +# connection went back to the pool, inside the one second window where the +# pool used to skip the alive check. The stray response has fully arrived +# about 750 ms in. + +http://%HOSTIP:%HTTPPORT/%TESTNUMBER http://%HOSTIP:%HTTPPORT/%TESTNUMBER0002 --rate 1/s + + + +# Verify data after the test has been "shot" + + +GET /%TESTNUMBER HTTP/1.1 +Host: %HOSTIP:%HTTPPORT +User-Agent: curl/%VERSION +Accept: */* + +GET /%TESTNUMBER0002 HTTP/1.1 +Host: %HOSTIP:%HTTPPORT +User-Agent: curl/%VERSION +Accept: */* + + + +first +second + + +