Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@amalshaji
Copy link
Owner

@amalshaji amalshaji commented Oct 21, 2025

Fixes #188

Summary by CodeRabbit

Release Notes

  • New Features
    • Added WebSocket upgrade support (HTTP 101 Switching Protocols) for tunneled connections
    • Added Server-Sent Events (SSE) streaming support for real-time event delivery without buffering

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 21, 2025

Walkthrough

The changes add specialized handling for WebSocket protocol upgrades (HTTP 101 Switching Protocols) and Server-Sent Events (SSE) streaming within the HTTP tunnel path. WebSocket connections are detected and immediately switched to TCP tunnel mode, while SSE streams are forwarded live without buffering. Regular HTTP responses maintain existing behavior.

Changes

Cohort / File(s) Summary
WebSocket Upgrade & SSE Streaming Support
tunnel/internal/client/ssh/ssh.go
Adds HTTP 101 status code detection to trigger WebSocket protocol switch via TCP tunnel; detects SSE (Content-Type: text/event-stream) and streams response body in real-time with per-chunk flushing; reconstructs status/headers for SSE (excluding Content-Length, Transfer-Encoding); imports strings package; removes post-response upgrade fallback block.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Tunnel
    participant Remote

    rect rgb(240, 248, 255)
    Note over Tunnel: HTTP Response Received
    end

    alt WebSocket Upgrade (101)
        Tunnel->>Tunnel: Check StatusCode == 101
        Tunnel->>Client: Write upgrade response
        Tunnel->>Client: Flush
        Tunnel->>Tunnel: Switch to TCP tunnel mode
        Tunnel->>Remote: Establish TCP tunnel
    else SSE Stream (Content-Type: text/event-stream)
        Tunnel->>Tunnel: Detect SSE content type
        Tunnel->>Client: Write status line & headers
        Tunnel->>Client: Write header terminator
        loop Stream Data
            Remote->>Tunnel: Read chunk
            Tunnel->>Client: Write chunk
            Tunnel->>Client: Flush immediately
        end
    else Regular HTTP Response
        Tunnel->>Tunnel: Read full response body
        Tunnel->>Client: Write status, headers, body
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Multiple distinct code paths (WebSocket upgrade, SSE streaming, regular HTTP) with conditional logic, streaming behavior with per-chunk flushing, header manipulation, and error handling require careful verification of protocol-specific edge cases and state transitions.

Poem

🐰 WebSockets now leap through tunnels with grace,
SSE streams flow at a real-time pace,
No buffering delays, just live data flow,
The tunnel's evolved—watch those connections glow! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Out of Scope Changes Check ❓ Inconclusive The summary indicates that the PR adds WebSocket upgrade handling (HTTP 101 Switching Protocols) in addition to SSE streaming support. However, the linked issue #188 specifically addresses only SSE behavior problems and does not mention WebSocket functionality. The WebSocket changes appear to be part of a broader tunnel refactoring, but without clear justification in the linked issue requirements, these changes may represent scope creep beyond the stated objective of fixing SSE tunneling. Review whether the WebSocket upgrade handling (HTTP 101) changes were necessary to properly implement the SSE fix or if they represent out-of-scope improvements. If WebSocket support is being added as an enhancement unrelated to the SSE issue, consider moving it to a separate PR to maintain clear scope boundaries.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "Fix sse tunneling" is directly related to the primary changes in the pull request. The summary clearly shows that the main modifications add Server-Sent Events (SSE) streaming support with proper header reconstruction and live data streaming without buffering, which directly addresses the issue. The title is concise and clearly communicates the core objective of fixing SSE tunnel behavior.
Linked Issues Check ✅ Passed The pull request directly addresses the requirements of issue #188. The linked issue reports that SSE connections remain open but no events are received through portr, with the expectation being to receive SSE messages as soon as the server pushes them. The code changes add SSE streaming support by detecting Content-Type containing "text/event-stream" and streaming the response body live from remote to source without buffering, including proper header reconstruction and per-chunk flushing. This implementation should resolve the reported issue where events were not being received by the client.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sse-ws-tunnel

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages
Copy link

Deploying portr-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: ff2f946
Status: ✅  Deploy successful!
Preview URL: https://4be2d603.portr-docs.pages.dev
Branch Preview URL: https://sse-ws-tunnel.portr-docs.pages.dev

View logs

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
tunnel/internal/client/ssh/ssh.go (2)

13-13: Import looks fine; consider adding mime for robust SSE detection.

If you adopt the SSE media-type parsing suggestion below, also import mime. Otherwise, keeping strings is fine.


314-360: SSE streaming path LGTM; tighten detection and close body on exit.

This likely resolves #188 by streaming without buffering. Make detection resilient and ensure cleanup.

Minimal patch:

@@
-  contentType := response.Header.Get("Content-Type")
-  if strings.Contains(contentType, "text/event-stream") {
+  // Robust SSE detection (case-insensitive, ignores parameters)
+  // Requires: import "mime"
+  ct, _, _ := mime.ParseMediaType(response.Header.Get("Content-Type"))
+  if strings.EqualFold(ct, "text/event-stream") {
@@
-    // Stream the body with immediate flushing for real-time delivery
+    // Stream the body with immediate flushing for real-time delivery
+    // Ensure the body is closed when we exit this branch
+    defer response.Body.Close()
     buf := make([]byte, 32*1024) // 32KB buffer
     for {
       n, err := response.Body.Read(buf)
       if n > 0 {
         _, writeErr := srcWriter.Write(buf[:n])
         if writeErr != nil {
           if s.config.Debug {
             s.logDebug("Failed to write SSE data", writeErr)
           }
           return
         }
         // Flush immediately to ensure real-time streaming
         srcWriter.Flush()
       }
       if err != nil {
         if err != io.EOF && s.config.Debug {
           s.logDebug("SSE stream ended", err)
         }
         break
       }
     }
     return
   }

And add the import:

 import (
@@
-  "strings"
+  "strings"
+  "mime"
 )

Optional: confirm if skipping s.logHttpRequest for SSE is intentional to avoid DB bloat; otherwise, consider metadata-only logging (status, headers, no body).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 168c5b1 and ff2f946.

📒 Files selected for processing (1)
  • tunnel/internal/client/ssh/ssh.go (2 hunks)
🔇 Additional comments (1)
tunnel/internal/client/ssh/ssh.go (1)

361-361: Regular HTTP path unchanged — OK.

No regressions spotted in the non-streaming branch.

Comment on lines +299 to +313
// Handle WebSocket upgrades and SSE streams with TCP tunneling
if response.StatusCode == http.StatusSwitchingProtocols {
// WebSocket upgrade - write response headers and switch to TCP tunneling
err = response.Write(srcWriter)
if err != nil {
if s.config.Debug {
s.logDebug("Failed to write WebSocket upgrade response", err)
}
return
}
srcWriter.Flush()
s.tcpTunnel(src, dst)
return
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

WebSocket upgrade: possible data loss due to buffered bytes not forwarded.

After http.ReadResponse, dstReader may already hold server WS frames; similarly, srcReader may hold client bytes beyond the HTTP request. Switching straight to tcpTunnel(src, dst) bypasses these buffers (io.Copy reads from net.Conn, not the bufio.Reader), dropping bytes and corrupting the stream.

Apply this patch to drain any buffered bytes on both sides before tunneling:

   if response.StatusCode == http.StatusSwitchingProtocols {
     // WebSocket upgrade - write response headers and switch to TCP tunneling
     err = response.Write(srcWriter)
     if err != nil {
       if s.config.Debug {
         s.logDebug("Failed to write WebSocket upgrade response", err)
       }
       return
     }
     srcWriter.Flush()
+    // Drain any bytes already buffered post-handshake to avoid loss when switching to raw TCP
+    if n := dstReader.Buffered(); n > 0 {
+      buf := make([]byte, n)
+      if _, err := io.ReadFull(dstReader, buf); err == nil {
+        if _, err := srcWriter.Write(buf); err != nil {
+          if s.config.Debug {
+            s.logDebug("Failed to flush buffered server bytes on WS upgrade", err)
+          }
+          return
+        }
+        srcWriter.Flush()
+      }
+    }
+    if n := srcReader.Buffered(); n > 0 {
+      buf := make([]byte, n)
+      if _, err := io.ReadFull(srcReader, buf); err == nil {
+        if _, err := dstWriter.Write(buf); err != nil {
+          if s.config.Debug {
+            s.logDebug("Failed to flush buffered client bytes on WS upgrade", err)
+          }
+          return
+        }
+        dstWriter.Flush()
+      }
+    }
     s.tcpTunnel(src, dst)
     return
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Handle WebSocket upgrades and SSE streams with TCP tunneling
if response.StatusCode == http.StatusSwitchingProtocols {
// WebSocket upgrade - write response headers and switch to TCP tunneling
err = response.Write(srcWriter)
if err != nil {
if s.config.Debug {
s.logDebug("Failed to write WebSocket upgrade response", err)
}
return
}
srcWriter.Flush()
s.tcpTunnel(src, dst)
return
}
// Handle WebSocket upgrades and SSE streams with TCP tunneling
if response.StatusCode == http.StatusSwitchingProtocols {
// WebSocket upgrade - write response headers and switch to TCP tunneling
err = response.Write(srcWriter)
if err != nil {
if s.config.Debug {
s.logDebug("Failed to write WebSocket upgrade response", err)
}
return
}
srcWriter.Flush()
// Drain any bytes already buffered post-handshake to avoid loss when switching to raw TCP
if n := dstReader.Buffered(); n > 0 {
buf := make([]byte, n)
if _, err := io.ReadFull(dstReader, buf); err == nil {
if _, err := srcWriter.Write(buf); err != nil {
if s.config.Debug {
s.logDebug("Failed to flush buffered server bytes on WS upgrade", err)
}
return
}
srcWriter.Flush()
}
}
if n := srcReader.Buffered(); n > 0 {
buf := make([]byte, n)
if _, err := io.ReadFull(srcReader, buf); err == nil {
if _, err := dstWriter.Write(buf); err != nil {
if s.config.Debug {
s.logDebug("Failed to flush buffered client bytes on WS upgrade", err)
}
return
}
dstWriter.Flush()
}
}
s.tcpTunnel(src, dst)
return
}
🤖 Prompt for AI Agents
In tunnel/internal/client/ssh/ssh.go around lines 299 to 313, the WebSocket
upgrade path switches directly to tcpTunnel(src, dst) which can drop data
already buffered in the bufio readers (dstReader/srcReader) after
http.ReadResponse; drain any buffered bytes from both readers into their
counterpart connections before starting the tunnel: for each side obtain the
buffered bytes (e.g. via Reader.Buffered/Peek or by using io.MultiReader to
prepend the buffer), write those bytes to the opposite writer/connection (handle
write errors and flush the srcWriter), then only call tcpTunnel(src, dst) once
buffered data has been forwarded and flushed. Ensure you log errors when
draining fails and return appropriately.

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.

Weird Server-Sent Events (SSE) behavior

1 participant