diff --git a/changelog.md b/changelog.md index d7d0249..0e44f8d 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-03-16 - 4.5.5 - fix(remoteingress-core) +wait for hub-to-client draining before cleanup and reliably send close frames + +- switch CLOSE frame delivery on the data channel from try_send to send().await to avoid dropping it when the channel is full +- delay stream cleanup until the hub-to-client task finishes or times out so large downstream responses continue after upload EOF +- add a bounded 5-minute wait for download draining to prevent premature termination of asymmetric transfers such as git fetch + ## 2026-03-15 - 4.5.4 - fix(remoteingress-core) preserve stream close ordering and add flow-control stall timeouts diff --git a/rust/crates/remoteingress-core/src/edge.rs b/rust/crates/remoteingress-core/src/edge.rs index a7453f6..ad0709f 100644 --- a/rust/crates/remoteingress-core/src/edge.rs +++ b/rust/crates/remoteingress-core/src/edge.rs @@ -665,7 +665,7 @@ async fn handle_client_connection( // After writing to client TCP, send WINDOW_UPDATE to hub so it can send more let hub_to_client_token = client_token.clone(); let wu_tx = tunnel_ctrl_tx.clone(); - let hub_to_client = tokio::spawn(async move { + let mut hub_to_client = tokio::spawn(async move { let mut consumed_since_update: u32 = 0; loop { tokio::select! { @@ -741,18 +741,32 @@ async fn handle_client_connection( } } - // Send CLOSE frame via DATA channel (must arrive AFTER last DATA for this stream) + // Send CLOSE frame via DATA channel (must arrive AFTER last DATA for this stream). + // Use send().await to guarantee delivery (try_send silently drops if channel full). if !client_token.is_cancelled() { let close_frame = encode_frame(stream_id, FRAME_CLOSE, &[]); - let _ = tunnel_data_tx.try_send(close_frame); + let _ = tunnel_data_tx.send(close_frame).await; } - // Cleanup + // Wait for the download task (hub → client) to finish draining all buffered + // response data. Upload EOF just means the client is done sending; the download + // must continue until all response data has been written to the client. + // This is critical for asymmetric transfers like git fetch (small request, large response). + // The download task will exit when: + // - back_rx returns None (back_tx dropped below after await, or hub sent CLOSE_BACK) + // - client_write fails (client disconnected) + // - client_token is cancelled + let _ = tokio::time::timeout( + Duration::from_secs(300), // 5 min max wait for download to finish + &mut hub_to_client, + ).await; + + // Now safe to clean up — download has finished or timed out { let mut writers = client_writers.lock().await; writers.remove(&stream_id); } - hub_to_client.abort(); + hub_to_client.abort(); // No-op if already finished; safety net if timeout fired let _ = edge_id; // used for logging context } diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 98327bb..1a19805 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/remoteingress', - version: '4.5.4', + version: '4.5.5', description: 'Edge ingress tunnel for DcRouter - accepts incoming TCP connections at network edge and tunnels them to DcRouter SmartProxy preserving client IP via PROXY protocol v1.' }