@@ -281,6 +281,39 @@ impl<C: StreamableHttpClient> StreamableHttpClientWorker<C> {
281281}
282282
283283impl < C : StreamableHttpClient > StreamableHttpClientWorker < C > {
284+ /// Convert a raw SSE stream into a JSON-RPC message stream without
285+ /// reconnection logic. Used for per-request POST SSE responses where
286+ /// we close the stream after the first response and want the underlying
287+ /// HTTP connection to be returned to the pool promptly.
288+ fn raw_sse_to_jsonrpc (
289+ stream : BoxedSseStream ,
290+ ) -> impl Stream < Item = Result < ServerJsonRpcMessage , StreamableHttpError < C :: Error > > > + Send + ' static
291+ {
292+ stream. filter_map ( |event| async {
293+ match event {
294+ Err ( e) => Some ( Err ( StreamableHttpError :: Sse ( e) ) ) ,
295+ Ok ( sse) => {
296+ let is_message =
297+ matches ! ( sse. event. as_deref( ) , None | Some ( "" ) | Some ( "message" ) ) ;
298+ if !is_message {
299+ return None ;
300+ }
301+ let data = sse. data ?;
302+ if data. trim ( ) . is_empty ( ) {
303+ return None ;
304+ }
305+ match serde_json:: from_str :: < ServerJsonRpcMessage > ( & data) {
306+ Ok ( msg) => Some ( Ok ( msg) ) ,
307+ Err ( e) => {
308+ tracing:: debug!( "failed to deserialize server message: {e}" ) ;
309+ None
310+ }
311+ }
312+ }
313+ }
314+ } )
315+ }
316+
284317 async fn execute_sse_stream (
285318 sse_stream : impl Stream < Item = Result < ServerJsonRpcMessage , StreamableHttpError < C :: Error > > >
286319 + Send
@@ -303,14 +336,26 @@ impl<C: StreamableHttpClient> StreamableHttpClientWorker<C> {
303336 let Some ( message) = message. transpose ( ) ? else {
304337 break ;
305338 } ;
306- let is_response = matches ! ( message, ServerJsonRpcMessage :: Response ( _) ) ;
339+ let is_response = matches ! (
340+ message,
341+ ServerJsonRpcMessage :: Response ( _) | ServerJsonRpcMessage :: Error ( _)
342+ ) ;
307343 let yield_result = sse_worker_tx. send ( message) . await ;
308344 if yield_result. is_err ( ) {
309345 tracing:: trace!( "streamable http transport worker dropped, exiting" ) ;
310346 break ;
311347 }
312348 if close_on_response && is_response {
313- tracing:: debug!( "got response, closing sse stream" ) ;
349+ tracing:: debug!( "got response, draining sse stream for connection reuse" ) ;
350+ // Drain remaining stream bytes so the HTTP/1.1 connection can
351+ // be returned to the pool instead of being discarded. The
352+ // server closes the channel shortly after sending the response,
353+ // so this normally completes in microseconds on localhost. The
354+ // timeout guards against servers that keep the stream open.
355+ let _ = tokio:: time:: timeout ( std:: time:: Duration :: from_millis ( 50 ) , async {
356+ while sse_stream. next ( ) . await . is_some ( ) { }
357+ } )
358+ . await ;
314359 break ;
315360 }
316361 }
@@ -718,38 +763,12 @@ impl<C: StreamableHttpClient> Worker for StreamableHttpClientWorker<C> {
718763 Ok ( ( ) )
719764 }
720765 Ok ( StreamableHttpPostResponse :: Sse ( stream, ..) ) => {
721- if let Some ( sid) = & session_id {
722- let sse_stream = SseAutoReconnectStream :: new (
723- stream,
724- StreamableHttpClientReconnect {
725- client : self . client . clone ( ) ,
726- session_id : sid. clone ( ) ,
727- uri : config. uri . clone ( ) ,
728- auth_header : config. auth_header . clone ( ) ,
729- custom_headers : protocol_headers
730- . clone ( ) ,
731- } ,
732- self . config . retry_config . clone ( ) ,
733- ) ;
734- streams. spawn ( Self :: execute_sse_stream (
735- sse_stream,
736- sse_worker_tx. clone ( ) ,
737- true ,
738- transport_task_ct. child_token ( ) ,
739- ) ) ;
740- } else {
741- let sse_stream =
742- SseAutoReconnectStream :: never_reconnect (
743- stream,
744- StreamableHttpError :: < C :: Error > :: UnexpectedEndOfStream ,
745- ) ;
746- streams. spawn ( Self :: execute_sse_stream (
747- sse_stream,
748- sse_worker_tx. clone ( ) ,
749- true ,
750- transport_task_ct. child_token ( ) ,
751- ) ) ;
752- }
766+ streams. spawn ( Self :: execute_sse_stream (
767+ Self :: raw_sse_to_jsonrpc ( stream) ,
768+ sse_worker_tx. clone ( ) ,
769+ true ,
770+ transport_task_ct. child_token ( ) ,
771+ ) ) ;
753772 tracing:: trace!( "got new sse stream after re-init" ) ;
754773 Ok ( ( ) )
755774 }
@@ -769,36 +788,16 @@ impl<C: StreamableHttpClient> Worker for StreamableHttpClientWorker<C> {
769788 Ok ( ( ) )
770789 }
771790 Ok ( StreamableHttpPostResponse :: Sse ( stream, ..) ) => {
772- if let Some ( session_id) = & session_id {
773- let sse_stream = SseAutoReconnectStream :: new (
774- stream,
775- StreamableHttpClientReconnect {
776- client : self . client . clone ( ) ,
777- session_id : session_id. clone ( ) ,
778- uri : config. uri . clone ( ) ,
779- auth_header : config. auth_header . clone ( ) ,
780- custom_headers : protocol_headers. clone ( ) ,
781- } ,
782- self . config . retry_config . clone ( ) ,
783- ) ;
784- streams. spawn ( Self :: execute_sse_stream (
785- sse_stream,
786- sse_worker_tx. clone ( ) ,
787- true ,
788- transport_task_ct. child_token ( ) ,
789- ) ) ;
790- } else {
791- let sse_stream = SseAutoReconnectStream :: never_reconnect (
792- stream,
793- StreamableHttpError :: < C :: Error > :: UnexpectedEndOfStream ,
794- ) ;
795- streams. spawn ( Self :: execute_sse_stream (
796- sse_stream,
797- sse_worker_tx. clone ( ) ,
798- true ,
799- transport_task_ct. child_token ( ) ,
800- ) ) ;
801- }
791+ // Per-request POST SSE streams use a thin
792+ // adapter instead of SseAutoReconnectStream so
793+ // the stream ends immediately when the server
794+ // closes the channel, enabling connection reuse.
795+ streams. spawn ( Self :: execute_sse_stream (
796+ Self :: raw_sse_to_jsonrpc ( stream) ,
797+ sse_worker_tx. clone ( ) ,
798+ true ,
799+ transport_task_ct. child_token ( ) ,
800+ ) ) ;
802801 tracing:: trace!( "got new sse stream" ) ;
803802 Ok ( ( ) )
804803 }
0 commit comments