11use std:: {
2- borrow:: Cow , collections:: HashMap , convert:: Infallible , fmt:: Display , sync:: Arc , time:: Duration ,
2+ borrow:: Cow ,
3+ collections:: HashMap ,
4+ convert:: Infallible ,
5+ fmt:: Display ,
6+ pin:: Pin ,
7+ sync:: Arc ,
8+ task:: { Context , Poll } ,
9+ time:: Duration ,
310} ;
411
512use bytes:: Bytes ;
6- use futures:: { StreamExt , future:: BoxFuture } ;
13+ use futures:: { Stream , StreamExt , future:: BoxFuture } ;
714use http:: { HeaderMap , Method , Request , Response , header:: ALLOW } ;
815use http_body:: Body ;
916use http_body_util:: { BodyExt , Full , combinators:: BoxBody } ;
17+ use pin_project_lite:: pin_project;
1018use tokio_stream:: wrappers:: ReceiverStream ;
1119use tokio_util:: sync:: CancellationToken ;
1220
@@ -22,7 +30,7 @@ use crate::{
2230 ProtocolVersion , RequestId , ServerJsonRpcMessage ,
2331 } ,
2432 serve_server,
25- service:: serve_directly ,
33+ service:: serve_directly_with_ct ,
2634 transport:: {
2735 OneshotTransport , TransportAdapterIdentity ,
2836 common:: {
@@ -852,14 +860,28 @@ where
852860 request. request . extensions_mut ( ) . insert ( parts) ;
853861 let ( transport, mut receiver) =
854862 OneshotTransport :: < RoleServer > :: new ( ClientJsonRpcMessage :: Request ( request) ) ;
855- let service = serve_directly ( service, transport, peer_info) ;
863+ // Give this stateless request its own cancellation token so a client
864+ // disconnect can cancel the in-flight handler (#857), as in the
865+ // non-negotiated stateless path below.
866+ let request_ct = CancellationToken :: new ( ) ;
867+ let service = serve_directly_with_ct ( service, transport, peer_info, request_ct. clone ( ) ) ;
856868 tokio:: spawn ( async move {
857869 let _ = service. waiting ( ) . await ;
858870 } ) ;
859871
860872 let cancel = self . config . cancellation_token . child_token ( ) ;
873+ // Cancel the handler if the client disconnects while it is still
874+ // producing its first message (this future is dropped before
875+ // `receiver.recv()` completes). Disarmed once the handler emits
876+ // anything, so a normal response is never cancelled.
877+ let mut disconnect_guard = Some ( request_ct. clone ( ) . drop_guard ( ) ) ;
861878 let first = tokio:: select! {
862- message = receiver. recv( ) => message,
879+ message = receiver. recv( ) => {
880+ if let Some ( guard) = disconnect_guard. take( ) {
881+ guard. disarm( ) ;
882+ }
883+ message
884+ }
863885 _ = cancel. cancelled( ) => None ,
864886 }
865887 . ok_or_else ( || {
@@ -870,17 +892,26 @@ where
870892 } ) ?;
871893
872894 if self . config . json_response || jsonrpc_http_status ( & first) != http:: StatusCode :: OK {
895+ // This message is the whole reply, so `receiver` is dropped here and
896+ // anything the handler emits afterwards is undeliverable. Cancel it so
897+ // a still-running handler stops instead of running on unobserved: its
898+ // terminal `send` would otherwise fail before adding the termination
899+ // permit, leaving the serve loop parked forever. A no-op when the
900+ // handler already completed.
901+ request_ct. cancel ( ) ;
873902 return jsonrpc_message_response ( first, true ) ;
874903 }
875904
905+ // The handler may still be streaming, so guard the response: dropping it
906+ // (client disconnect) must cancel the handler.
876907 let stream = futures:: stream:: once ( async move { first } )
877908 . chain ( ReceiverStream :: new ( receiver) )
878909 . map ( |message| {
879910 tracing:: trace!( ?message) ;
880911 ServerSseMessage :: from_message ( message)
881912 } ) ;
882913 Ok ( sse_stream_response (
883- stream,
914+ CancelOnDisconnect :: new ( stream, request_ct ) ,
884915 self . config . sse_keep_alive ,
885916 self . config . cancellation_token . child_token ( ) ,
886917 ) )
@@ -1544,7 +1575,13 @@ where
15441575 request. request . extensions_mut ( ) . insert ( part) ;
15451576 let ( transport, mut receiver) =
15461577 OneshotTransport :: < RoleServer > :: new ( ClientJsonRpcMessage :: Request ( request) ) ;
1547- let service = serve_directly ( service, transport, peer_info) ;
1578+ // Give this stateless request its own cancellation token so a
1579+ // client disconnect can cancel the in-flight handler (#857). A
1580+ // stateless request is one-shot (no session, no resumption), so a
1581+ // dropped response is terminal and safe to cancel.
1582+ let request_ct = CancellationToken :: new ( ) ;
1583+ let service =
1584+ serve_directly_with_ct ( service, transport, peer_info, request_ct. clone ( ) ) ;
15481585 tokio:: spawn ( async move {
15491586 // on service created
15501587 let _ = service. waiting ( ) . await ;
@@ -1554,8 +1591,19 @@ where
15541591 // emits an intermediate notification or request, preserve
15551592 // the complete message sequence by falling back to SSE.
15561593 let cancel = self . config . cancellation_token . child_token ( ) ;
1594+ // Cancel the handler if the client disconnects while it is
1595+ // still producing its first message (this future is dropped
1596+ // before `receiver.recv()` completes). Disarmed once the
1597+ // handler emits anything, so a normal response is never
1598+ // cancelled.
1599+ let mut disconnect_guard = Some ( request_ct. clone ( ) . drop_guard ( ) ) ;
15571600 let Some ( message) = ( tokio:: select! {
1558- res = receiver. recv( ) => res,
1601+ res = receiver. recv( ) => {
1602+ if let Some ( guard) = disconnect_guard. take( ) {
1603+ guard. disarm( ) ;
1604+ }
1605+ res
1606+ }
15591607 _ = cancel. cancelled( ) => None ,
15601608 } ) else {
15611609 return Err ( internal_error_response ( "empty response" ) (
@@ -1579,6 +1627,9 @@ where
15791627 . body ( Full :: new ( Bytes :: from ( body) ) . boxed ( ) )
15801628 . expect ( "valid response" ) )
15811629 } else {
1630+ // The handler emitted an intermediate message and is still
1631+ // running, so guard the streamed sequence too: dropping it
1632+ // (client disconnect) must cancel the handler.
15821633 let first = futures:: stream:: once ( async move {
15831634 ServerSseMessage :: from_message ( message)
15841635 } ) ;
@@ -1587,17 +1638,19 @@ where
15871638 ServerSseMessage :: from_message ( message)
15881639 } ) ;
15891640 Ok ( sse_stream_response (
1590- first. chain ( remaining) ,
1641+ CancelOnDisconnect :: new ( first. chain ( remaining) , request_ct ) ,
15911642 self . config . sse_keep_alive ,
15921643 self . config . cancellation_token . child_token ( ) ,
15931644 ) )
15941645 }
15951646 } else {
1596- // SSE mode (default): original behaviour preserved unchanged
1647+ // SSE mode (default): cancel the handler if the client
1648+ // disconnects (drops the response stream) before it completes.
15971649 let stream = ReceiverStream :: new ( receiver) . map ( |message| {
15981650 tracing:: trace!( ?message) ;
15991651 ServerSseMessage :: from_message ( message)
16001652 } ) ;
1653+ let stream = CancelOnDisconnect :: new ( stream, request_ct) ;
16011654 Ok ( sse_stream_response (
16021655 stream,
16031656 self . config . sse_keep_alive ,
@@ -1680,3 +1733,52 @@ where
16801733 } )
16811734 }
16821735}
1736+
1737+ pin_project ! {
1738+ /// Wraps a stateless SSE response stream so a client disconnect cancels the
1739+ /// in-flight request.
1740+ ///
1741+ /// A stateless streamable-HTTP request is one-shot: it has no session and no
1742+ /// resumption, so a dropped response stream means the client is gone for
1743+ /// good. When the stream is dropped *before* it ends naturally, the request's
1744+ /// cancellation token is fired, which stops the dedicated `serve_directly`
1745+ /// loop and cancels the handler's `RequestContext::ct` (see #857). If the
1746+ /// stream ends naturally (the request completed), the guard is disarmed so
1747+ /// normal completion cancels nothing.
1748+ struct CancelOnDisconnect <S > {
1749+ #[ pin]
1750+ inner: S ,
1751+ ct: Option <CancellationToken >,
1752+ }
1753+ impl <S > PinnedDrop for CancelOnDisconnect <S > {
1754+ fn drop( this: Pin <& mut Self >) {
1755+ let this = this. project( ) ;
1756+ if let Some ( ct) = this. ct. take( ) {
1757+ ct. cancel( ) ;
1758+ }
1759+ }
1760+ }
1761+ }
1762+
1763+ impl < S > CancelOnDisconnect < S > {
1764+ fn new ( inner : S , ct : CancellationToken ) -> Self {
1765+ Self {
1766+ inner,
1767+ ct : Some ( ct) ,
1768+ }
1769+ }
1770+ }
1771+
1772+ impl < S : Stream > Stream for CancelOnDisconnect < S > {
1773+ type Item = S :: Item ;
1774+
1775+ fn poll_next ( self : Pin < & mut Self > , cx : & mut Context < ' _ > ) -> Poll < Option < Self :: Item > > {
1776+ let this = self . project ( ) ;
1777+ let polled = this. inner . poll_next ( cx) ;
1778+ if let Poll :: Ready ( None ) = & polled {
1779+ // Ended naturally: the request completed, so don't cancel on drop.
1780+ * this. ct = None ;
1781+ }
1782+ polled
1783+ }
1784+ }
0 commit comments