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>
}
+/* 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)
{
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);
}
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.
*/