]> git.kaiwu.me - haproxy.git/commitdiff
BUG/MEDIUM: mux-fcgi: do not enforce the timeout on connections with streams
authorAleksandar Lazic <al-haproxy@none.at>
Tue, 3 Nov 2026 13:00:00 +0000 (14:00 +0100)
committerChristopher Faulet <cfaulet@haproxy.com>
Fri, 6 Nov 2026 10:10:56 +0000 (11:10 +0100)
The FCGI connection task is rearmed with "timeout server" on each pass
in fcgi_process(), and fcgi_timeout_task() kills the connection when it
expires, even when streams are still attached to it. With a slow
client, HAProxy stops reading from the FastCGI server once the buffers
are full, so nothing happens on the FCGI connection for a while, and
the connection is killed in the middle of the response. The response is
truncated, the log reports "SD--", and the FastCGI server gets a reset
while it is blocked in write().

Whether the stream waits for the server or for the client is only known
by the stream layer, which already enforces "timeout server" when the
server does not answer. So, as done for the H2 mux by commit c2ea47fb1
("BUG/MEDIUM: mux-h2: do not enforce timeout on long connections", for
GitHub issue #308), the connection timeout is now only enforced when no
stream connector is attached anymore: fcgi_conn_may_expire() mirrors
h2c_may_expire(), fcgi_process() leaves the task unarmed while streams
are attached, and fcgi_timeout_task() does not kill the connection in
this case.

A FastCGI server which does not answer at all still gets a 504 after
"timeout server", reported as "sH--", as before.

This fixes GitHub issue #3558. It should be backported to all supported
versions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Aleksandar Lazic <al-haproxy@none.at>
src/mux_fcgi.c

index cc92ac8ef7ef5177a86a0bc0d9db0ca185dd54b3..d03adb7f94b567a633dc74056057feefd9a75d72 100644 (file)
@@ -834,6 +834,15 @@ static int fcgi_conn_read0_pending(struct fcgi_conn *fconn)
 }
 
 
+/* returns true if the connection may expire, i.e. if no stream connector is
+ * attached anymore. As long as one is attached, the timeouts are enforced by
+ * the stream layer, which knows if it waits for the server or for the client.
+ */
+static inline int fcgi_conn_may_expire(const struct fcgi_conn *fconn)
+{
+       return !fconn->nb_sc;
+}
+
 /* Returns true if the FCGI connection must be release */
 static inline int fcgi_conn_is_dead(struct fcgi_conn *fconn)
 {
@@ -3247,7 +3256,10 @@ static int fcgi_process(struct fcgi_conn *fconn)
                fcgi_release_mbuf(fconn);
 
        if (fconn->task) {
-               fconn->task->expire = tick_add(now_ms, (fconn->state == FCGI_CS_CLOSED ? fconn->shut_timeout : fconn->timeout));
+               if (fcgi_conn_may_expire(fconn))
+                       fconn->task->expire = tick_add(now_ms, (fconn->state == FCGI_CS_CLOSED ? fconn->shut_timeout : fconn->timeout));
+               else
+                       fconn->task->expire = TICK_ETERNITY;
                task_queue(fconn->task);
        }
 
@@ -3358,6 +3370,16 @@ struct task *fcgi_timeout_task(struct task *t, void *context, unsigned int state
                        return t;
                }
 
+               if (!fcgi_conn_may_expire(fconn)) {
+                       /* streams are still attached, they may be waiting for
+                        * the client, so the timeout is not ours to enforce.
+                        */
+                       HA_SPIN_UNLOCK(IDLE_CONNS_LOCK, &idle_conns[tid].idle_conns_lock);
+                       t->expire = TICK_ETERNITY;
+                       TRACE_DEVEL("leaving (streams attached)", FCGI_EV_FCONN_WAKE, fconn->conn);
+                       return t;
+               }
+
                /* We're about to destroy the connection, so make sure nobody attempts
                 * to steal it from us.
                 */