fix(rustproxy-http): prevent HTTP/3 response body streaming from hanging on backend completion

This commit is contained in:
2026-03-20 03:19:57 +00:00
parent 81de611255
commit f3cd4d193e
3 changed files with 49 additions and 5 deletions

View File

@@ -259,6 +259,12 @@ async fn handle_h3_request(
h3_response = h3_response.header(name, value);
}
// Extract content-length for body loop termination (must be before into_body())
let content_length: Option<u64> = response.headers()
.get(hyper::header::CONTENT_LENGTH)
.and_then(|v| v.to_str().ok())
.and_then(|s| s.parse().ok());
// Add Alt-Svc for HTTP/3 advertisement
let alt_svc = route.action.udp.as_ref()
.and_then(|u| u.quic.as_ref())
@@ -279,21 +285,52 @@ async fn handle_h3_request(
// Stream response body back
use http_body_util::BodyExt;
use http_body::Body as _;
let mut body = response.into_body();
let mut total_bytes_out: u64 = 0;
while let Some(frame) = body.frame().await {
match frame {
Ok(frame) => {
// Per-frame idle timeout: if no frame arrives within this duration, assume
// the body is complete (or the backend has stalled). This prevents indefinite
// hangs on close-delimited bodies or when hyper's internal trailers oneshot
// never resolves after all data has been received.
const FRAME_IDLE_TIMEOUT: Duration = Duration::from_secs(30);
loop {
// Layer 1: If the body already knows it is finished (Content-Length
// bodies track remaining bytes internally), break immediately to
// avoid blocking on hyper's internal trailers oneshot.
if body.is_end_stream() {
break;
}
// Layer 3: Per-frame idle timeout safety net
match tokio::time::timeout(FRAME_IDLE_TIMEOUT, body.frame()).await {
Ok(Some(Ok(frame))) => {
if let Some(data) = frame.data_ref() {
total_bytes_out += data.len() as u64;
stream.send_data(Bytes::copy_from_slice(data)).await
.map_err(|e| anyhow::anyhow!("Failed to send H3 data: {}", e))?;
// Layer 2: Content-Length byte count check
if let Some(cl) = content_length {
if total_bytes_out >= cl {
break;
}
}
}
}
Err(e) => {
Ok(Some(Err(e))) => {
warn!("Backend body read error: {}", e);
break;
}
Ok(None) => break, // Body ended naturally
Err(_) => {
debug!(
"H3 body frame idle timeout ({:?}) after {} bytes; finishing stream",
FRAME_IDLE_TIMEOUT, total_bytes_out
);
break;
}
}
}