Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c63f6fcd5f | |||
| f3cd4d193e |
@@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-20 - 25.17.4 - fix(rustproxy-http)
|
||||||
|
prevent HTTP/3 response body streaming from hanging on backend completion
|
||||||
|
|
||||||
|
- extract and track Content-Length before consuming the response body
|
||||||
|
- stop the HTTP/3 body loop when the stream reports end-of-stream or the expected byte count has been sent
|
||||||
|
- add a per-frame idle timeout to avoid indefinite waits on stalled or close-delimited backend bodies
|
||||||
|
|
||||||
## 2026-03-20 - 25.17.3 - fix(repository)
|
## 2026-03-20 - 25.17.3 - fix(repository)
|
||||||
no changes detected
|
no changes detected
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@push.rocks/smartproxy",
|
"name": "@push.rocks/smartproxy",
|
||||||
"version": "25.17.3",
|
"version": "25.17.4",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.",
|
"description": "A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
|
|||||||
@@ -259,6 +259,12 @@ async fn handle_h3_request(
|
|||||||
h3_response = h3_response.header(name, value);
|
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
|
// Add Alt-Svc for HTTP/3 advertisement
|
||||||
let alt_svc = route.action.udp.as_ref()
|
let alt_svc = route.action.udp.as_ref()
|
||||||
.and_then(|u| u.quic.as_ref())
|
.and_then(|u| u.quic.as_ref())
|
||||||
@@ -279,21 +285,52 @@ async fn handle_h3_request(
|
|||||||
|
|
||||||
// Stream response body back
|
// Stream response body back
|
||||||
use http_body_util::BodyExt;
|
use http_body_util::BodyExt;
|
||||||
|
use http_body::Body as _;
|
||||||
let mut body = response.into_body();
|
let mut body = response.into_body();
|
||||||
let mut total_bytes_out: u64 = 0;
|
let mut total_bytes_out: u64 = 0;
|
||||||
while let Some(frame) = body.frame().await {
|
|
||||||
match frame {
|
// Per-frame idle timeout: if no frame arrives within this duration, assume
|
||||||
Ok(frame) => {
|
// 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() {
|
if let Some(data) = frame.data_ref() {
|
||||||
total_bytes_out += data.len() as u64;
|
total_bytes_out += data.len() as u64;
|
||||||
stream.send_data(Bytes::copy_from_slice(data)).await
|
stream.send_data(Bytes::copy_from_slice(data)).await
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to send H3 data: {}", e))?;
|
.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);
|
warn!("Backend body read error: {}", e);
|
||||||
break;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@push.rocks/smartproxy',
|
name: '@push.rocks/smartproxy',
|
||||||
version: '25.17.3',
|
version: '25.17.4',
|
||||||
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
|
description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user