Skip to content

fix: Eliminate 6s delay on concurrent H2 requests#3

Merged
dviejokfs merged 2 commits intomainfrom
feat/h2-passthrough-streaming
Mar 7, 2026
Merged

fix: Eliminate 6s delay on concurrent H2 requests#3
dviejokfs merged 2 commits intomainfrom
feat/h2-passthrough-streaming

Conversation

@dviejokfs
Copy link
Contributor

Summary

  • Exit node: Rewrote H2 proxy as true bidirectional passthrough — request headers are forwarded immediately without buffering the body, fixing the 6s stall caused by body_stream.data().await blocking on GET requests
  • Exit node: Fixed QUIC send half being dropped early (moved to spawned task), which caused 502 "Tunnel closed" on every request. Now uses mpsc channel + tokio::select! to keep quic_send alive for the full request lifetime
  • Client: Inject Connection: close on proxied HTTP/1.1 requests so local servers (Next.js, etc.) close the TCP connection after responding, instead of holding it open with keep-alive (~6s idle timeout). WebSocket upgrade requests are excluded

Before

All concurrent H2 requests stalled for ~6 seconds (serialized by body buffering + keep-alive)

After

Requests flow through instantly as a transparent bidirectional pipe

Test plan

  • Verified concurrent H2 page loads complete without delay
  • WebSocket connections still work (Connection: Upgrade preserved)
  • POST requests with body still forward correctly
  • Chunked transfer encoding still decoded for H2 compatibility

The previous passthrough rewrite moved quic_send into a spawned task
for body forwarding. For GET requests (no body), the task completed
immediately, dropping quic_send and closing the QUIC send half. The
client interpreted this as stream termination and closed before the
response arrived, causing 502 "Tunnel closed" on every request.

Fix: Use an mpsc channel to forward H2 body chunks to the main task,
which owns quic_send for the entire request lifetime. tokio::select!
handles both body forwarding and response streaming concurrently.
The transparent streaming proxy kept TCP connections alive (HTTP/1.1
default), so local servers like Next.js never closed the connection
after sending their response. This meant local_read.read() blocked
indefinitely, delaying the tunnel stream close by ~6 seconds.

Fix: Inject Connection: close header when rewriting Host headers for
non-upgrade requests. WebSocket upgrade requests preserve the original
Connection: Upgrade header.
@dviejokfs dviejokfs merged commit 521e826 into main Mar 7, 2026
1 of 2 checks passed
@dviejokfs dviejokfs deleted the feat/h2-passthrough-streaming branch March 7, 2026 11:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant