fix(rustproxy-http): prevent HTTP/3 response body streaming from hanging on backend completion
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user