From bf76275fff4869fa2925ab4d627b677d45e528d6 Mon Sep 17 00:00:00 2001 From: Amaury Denoyelle Date: Wed, 17 Jun 2026 18:04:56 +0200 Subject: [PATCH] BUG/MEDIUM: mux_quic: complete stream shutdown for read channel Prior to this patch, shut stream callback only handles write channel closure. In case of an early closure, a RESET_STREAM would be emitted. On the frontend side in most cases this is sufficient as read channel is already closed, as HTTP/3 GET requests has been fully received. However, this may not be the case for POST requests. Also, on the backend side, haproxy acts a client. In this case, a stream early closure will typically happen before receiving the full response. Nothing will be emitted (RESET_STREAM is unnecessary as write channel is already closed), thus the server peer will continue to emit. To fix this situation, the current patch implement read channel closure on shut if SE_SHR_RESET is set. Callback lclose from app_ops is called with a new dedicated mode for read channel closure, which will result in a STOP_SENDING frame generated by H3 and hq transcoders. This will instruct the peer to stop emission. This should be backported up to 3.3. Note that this depends on the following patch : dde3ee06c30f20091443bdafdda0e0294f7ac26b MINOR: mux_quic: use separate error code for STOP_SENDING --- include/haproxy/mux_quic-t.h | 1 + src/h3.c | 20 ++++++++++++++++++++ src/hq_interop.c | 4 ++++ src/mux_quic.c | 11 ++++++++++- 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/include/haproxy/mux_quic-t.h b/include/haproxy/mux_quic-t.h index 25bab15d9..7ec467386 100644 --- a/include/haproxy/mux_quic-t.h +++ b/include/haproxy/mux_quic-t.h @@ -220,6 +220,7 @@ enum qcc_app_ops_lclose_mode { QCC_APP_OPS_LCLO_MODE_NORMAL, QCC_APP_OPS_LCLO_MODE_ABORT, QCC_APP_OPS_LCLO_MODE_KILL_CONN, + QCC_APP_OPS_LCLO_MODE_READ, }; /* QUIC application layer operations */ diff --git a/src/h3.c b/src/h3.c index 32d8f33da..d11005eb0 100644 --- a/src/h3.c +++ b/src/h3.c @@ -3282,6 +3282,21 @@ static void h3_lclose(struct qcs *qcs, enum qcc_app_ops_lclose_mode mode) { TRACE_ENTER(H3_EV_H3S_END, qcs->qcc->conn, qcs); + /* RFC 9114 4.1.1. Request Cancellation and Rejection + * + * Servers MUST NOT use the H3_REQUEST_REJECTED error code for requests + * that were partially or fully processed. When a server abandons a + * response after partial processing, it SHOULD abort its response + * stream with the error code H3_REQUEST_CANCELLED. + * Client SHOULD use the error code H3_REQUEST_CANCELLED to cancel requests. + */ + + /* Following the above recommendations, H3_REQUEST_REJECTED is never + * used in practice in haproxy as a server on shut as upper stream + * layer is instantiated immediately with HEADERS content attached. The + * only exception is for stream closure after GOAWAY emission. + */ + switch (mode) { case QCC_APP_OPS_LCLO_MODE_NORMAL: /* Close stream with FIN. This can only be performed if at @@ -3312,6 +3327,11 @@ static void h3_lclose(struct qcs *qcs, enum qcc_app_ops_lclose_mode mode) muxc_tevt_type_graceful_shut); } break; + + case QCC_APP_OPS_LCLO_MODE_READ: + TRACE_STATE("request stream read channel closure", H3_EV_H3S_END, qcs->qcc->conn, qcs); + qcc_abort_stream_read(qcs, H3_ERR_REQUEST_CANCELLED); + break; } TRACE_LEAVE(H3_EV_H3S_END, qcs->qcc->conn, qcs); diff --git a/src/hq_interop.c b/src/hq_interop.c index 50c6791d8..4d2b4a80f 100644 --- a/src/hq_interop.c +++ b/src/hq_interop.c @@ -424,6 +424,10 @@ static void hq_interop_lclose(struct qcs *qcs, enum qcc_app_ops_lclose_mode mode if (!(qcs->qcc->flags & (QC_CF_ERR_CONN|QC_CF_ERRL))) qcc_set_error(qcs->qcc, 0, 0, muxc_tevt_type_graceful_shut); break; + + case QCC_APP_OPS_LCLO_MODE_READ: + qcc_abort_stream_read(qcs, 0); + break; } } diff --git a/src/mux_quic.c b/src/mux_quic.c index 6960c5994..dcf21a726 100644 --- a/src/mux_quic.c +++ b/src/mux_quic.c @@ -4711,8 +4711,12 @@ static void qcm_strm_shut(struct stconn *sc, unsigned int mode, struct se_abort_ TRACE_ENTER(QMUX_EV_STRM_SHUT, qcc->conn, qcs); + /* Not implemented. */ + BUG_ON(mode & SE_SHR_DRAIN); + /* Early closure reported if QC_SF_FIN_STREAM not yet set. */ - if (!qcs_is_close_local(qcs) && + if ((mode & (SE_SHW_SILENT|SE_SHW_NORMAL)) && + !qcs_is_close_local(qcs) && !(qcs->flags & (QC_SF_FIN_STREAM|QC_SF_TO_RESET))) { if (qcs->flags & QC_SF_UNKNOWN_PL_LENGTH) @@ -4726,6 +4730,11 @@ static void qcm_strm_shut(struct stconn *sc, unsigned int mode, struct se_abort_ tasklet_wakeup(qcc->wait_event.tasklet); } + if ((mode & SE_SHR_RESET) && + !qcs_is_close_remote(qcs) && !(qcs->flags & (QC_SF_READ_ABORTED))) { + qcc->app_ops->lclose(qcs, QCC_APP_OPS_LCLO_MODE_READ); + } + out: TRACE_LEAVE(QMUX_EV_STRM_SHUT, qcc->conn, qcs); }