From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id 7F6801FF0A8 for ; Sat, 17 Oct 2026 18:17:08 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id D0C2721478; Sat, 17 Oct 2026 18:17:04 +0200 (CEST) From: Dietmar Maurer To: pve-devel@lists.proxmox.com Subject: [PATCH rest-server] rest-server: stop a json-seq stream once its client is gone Date: Sat, 17 Oct 2026 18:16:56 +0200 Message-ID: <20261017161656.3529676-1-dietmar@proxmox.com> X-Mailer: git-send-email 2.47.3 MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 2 AWL -0.237 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods RDNS_NONE 1.274 Delivered to internal network by a host with no rDNS SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record Message-ID-Hash: 3XI6J73HNLYH3TNJ6V2FDQASCYBVS452 X-Message-ID-Hash: 3XI6J73HNLYH3TNJ6V2FDQASCYBVS452 X-MailFrom: dietmar@zilli.proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: The task forwarding the records of a streamed response only noticed a closed connection when sending the next record failed. A stream that stays silent for a long time, such as an API handler waiting on a slow backend, thus kept running after its client went away, and so did the work behind it, as the producer only learns of the gone client once the stream is dropped. Stop forwarding as soon as the client is gone, which drops the stream right away. Signed-off-by: Dietmar Maurer --- proxmox-rest-server/src/rest.rs | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/proxmox-rest-server/src/rest.rs b/proxmox-rest-server/src/rest.rs index 779851eb..f47761bd 100644 --- a/proxmox-rest-server/src/rest.rs +++ b/proxmox-rest-server/src/rest.rs @@ -896,7 +896,16 @@ fn handle_stream_as_json_seq(stream: proxmox_router::Stream) -> Result record, + () = send.closed() => break, + }; + let Some(record) = record else { + break; + }; if send.send(Ok(record.to_bytes())).await.is_err() { break; } @@ -1475,6 +1484,21 @@ impl Unformatted { mod tests { use super::*; + #[tokio::test] + async fn json_seq_stream_stops_once_the_client_is_gone() { + let (send, receive) = tokio::sync::mpsc::unbounded_channel::(); + let stream = tokio_stream::wrappers::UnboundedReceiverStream::new(receive); + let response = handle_stream_as_json_seq(proxmox_router::Stream::from(stream)).unwrap(); + tokio::task::yield_now().await; + assert!(!send.is_closed()); + + // the stream sends nothing, yet it is dropped with the response + drop(response); + tokio::time::timeout(std::time::Duration::from_secs(5), send.closed()) + .await + .expect("stream of a gone client still running"); + } + fn headers(name: &'static str, value: &str) -> HeaderMap { let mut headers = HeaderMap::new(); headers.insert(name, value.parse().unwrap()); -- 2.47.3