@@ -73,6 +73,15 @@ public HttpTunnelClient(HttpTunnelRequest tunnel, LogLevel? logLevel)
7373 return Task . CompletedTask ;
7474 } ) ;
7575
76+ Connection . On < SseConnection > ( "NewSseConnection" , ( sseConnection ) =>
77+ {
78+ LogRequest ? . Invoke ( "SSE" , sseConnection . Path ) ;
79+
80+ _ = TunnelSseConnectionAsync ( sseConnection ) ;
81+
82+ return Task . CompletedTask ;
83+ } ) ;
84+
7685 Connection . Reconnected += async connectionId =>
7786 {
7887 _currentTunnel = await RegisterTunnelAsync ( tunnel ) ;
@@ -260,6 +269,84 @@ private async Task StreamOutgoingWsAsync(WebSocket localWebSocket, WsConnection
260269 }
261270 }
262271
272+ private async Task TunnelSseConnectionAsync ( SseConnection sseConnection )
273+ {
274+ using var cts = new CancellationTokenSource ( ) ;
275+
276+ try
277+ {
278+ // Send the request to the local server
279+ using var request = new HttpRequestMessage ( new HttpMethod ( sseConnection . Method ) , sseConnection . Path ) ;
280+
281+ request . Headers . Accept . Add ( new MediaTypeWithQualityHeaderValue ( "text/event-stream" ) ) ;
282+
283+ request . Content = new StringContent ( sseConnection . Content ) ;
284+ if ( sseConnection . ContentType != null )
285+ {
286+ request . Content . Headers . ContentType = MediaTypeHeaderValue . Parse ( sseConnection . ContentType ) ;
287+ }
288+
289+ using var response = await LocalHttpClient . SendAsync (
290+ request ,
291+ HttpCompletionOption . ResponseHeadersRead ,
292+ cts . Token ) ;
293+
294+ response . EnsureSuccessStatusCode ( ) ;
295+
296+ // Stream the SSE data from local server to the public tunnel
297+ var outgoingTask = StreamOutgoingSseAsync ( response , sseConnection , cts . Token ) ;
298+
299+ // Wait for the streaming to complete
300+ await outgoingTask ;
301+ }
302+ catch ( Exception ex )
303+ {
304+ LogFailedRequest ? . Invoke ( "SSE" , sseConnection . Path ) ;
305+ LogException ? . Invoke ( ex ) ;
306+ }
307+ finally
308+ {
309+ await cts . CancelAsync ( ) ;
310+
311+ Log ? . Invoke ( $ "[SSE] Connection { sseConnection . RequestId } closed.") ;
312+ }
313+ }
314+
315+ private async Task StreamOutgoingSseAsync ( HttpResponseMessage response , SseConnection sseConnection , CancellationToken cancellationToken )
316+ {
317+ await Connection . InvokeAsync (
318+ "StreamOutgoingSseAsync" ,
319+ StreamLocalSseAsync ( response , sseConnection , cancellationToken ) ,
320+ sseConnection ,
321+ cancellationToken : cancellationToken ) ;
322+ }
323+
324+ private async IAsyncEnumerable < ReadOnlyMemory < byte > > StreamLocalSseAsync (
325+ HttpResponseMessage response ,
326+ SseConnection sseConnection ,
327+ [ EnumeratorCancellation ] CancellationToken cancellationToken )
328+ {
329+ const int chunkSize = 32 * 1024 ;
330+ byte [ ] buffer = ArrayPool < byte > . Shared . Rent ( chunkSize ) ;
331+
332+ try
333+ {
334+ using var stream = await response . Content . ReadAsStreamAsync ( cancellationToken ) ;
335+
336+ int bytesRead ;
337+ while ( ( bytesRead = await stream . ReadAsync ( buffer , 0 , buffer . Length , cancellationToken ) ) > 0 )
338+ {
339+ yield return new ReadOnlyMemory < byte > ( buffer , 0 , bytesRead ) ;
340+ }
341+ }
342+ finally
343+ {
344+ Log ? . Invoke ( $ "[SSE] Reading data from connection { sseConnection . RequestId } finished.") ;
345+
346+ ArrayPool < byte > . Shared . Return ( buffer ) ;
347+ }
348+ }
349+
263350 private async Task < HttpTunnelResponse ? > RegisterTunnelAsync ( HttpTunnelRequest tunnel )
264351 {
265352 tunnel . Subdomain = _currentTunnel ? . Subdomain ;
0 commit comments