@@ -176,6 +176,23 @@ pub(crate) fn in_request_handler_scope() -> bool {
176176 ORIGINATING_REQUEST . try_with ( |_| ( ) ) . is_ok ( )
177177}
178178
179+ /// Marker stored in an outbound request's [`Extensions`](crate::model::Extensions)
180+ /// identifying the in-flight peer request it was issued from (SEP-2260).
181+ ///
182+ /// Attached automatically whenever a request is sent from within a request
183+ /// handler. The streamable HTTP server uses it to deliver server-initiated
184+ /// requests on the originating client request's SSE stream.
185+ ///
186+ /// # In-memory only
187+ ///
188+ /// `Extensions` are never serialized, so this marker does not appear on the
189+ /// wire (SEP-2260 defines no wire field). Session managers that serialize
190+ /// messages between processes lose it; such requests fall back to the
191+ /// standalone stream (logged as a warning).
192+ #[ derive( Debug , Clone , PartialEq , Eq ) ]
193+ #[ expect( clippy:: exhaustive_structs, reason = "intentionally exhaustive" ) ]
194+ pub struct OriginatingRequestId ( pub RequestId ) ;
195+
179196pub type TxJsonRpcMessage < R > =
180197 JsonRpcMessage < <R as ServiceRole >:: Req , <R as ServiceRole >:: Resp , <R as ServiceRole >:: Not > ;
181198pub type RxJsonRpcMessage < R > = JsonRpcMessage <
@@ -747,6 +764,11 @@ impl<R: ServiceRole> Peer<R> {
747764 self . peer_info ( ) . as_deref ( ) ,
748765 in_request_handler_scope ( ) ,
749766 ) ?;
767+ if let Ok ( originating) = ORIGINATING_REQUEST . try_with ( |id| id. clone ( ) ) {
768+ request
769+ . extensions_mut ( )
770+ . insert ( OriginatingRequestId ( originating) ) ;
771+ }
750772 let id = self . request_id_provider . next_request_id ( ) ;
751773 let progress_token = self . progress_token_provider . next_progress_token ( ) ;
752774 if let Some ( metadata) = self . client_request_metadata . get ( ) {
@@ -1631,3 +1653,51 @@ where
16311653 dg : ct. drop_guard ( ) ,
16321654 }
16331655}
1656+
1657+ #[ cfg( all( test, feature = "server" ) ) ]
1658+ mod sep2260_marker_tests {
1659+ use std:: sync:: Arc ;
1660+
1661+ use super :: * ;
1662+ use crate :: model:: { PingRequest , RequestId , ServerRequest } ;
1663+
1664+ fn ping ( ) -> ServerRequest {
1665+ ServerRequest :: PingRequest ( PingRequest {
1666+ method : Default :: default ( ) ,
1667+ extensions : Default :: default ( ) ,
1668+ } )
1669+ }
1670+
1671+ async fn send_and_capture ( scope : Option < RequestId > ) -> <RoleServer as ServiceRole >:: Req {
1672+ // peer_info None => enforce_request_association is non-strict, so the
1673+ // send is accepted regardless of scope; we only inspect the sink message.
1674+ let ( peer, mut rx) =
1675+ Peer :: < RoleServer > :: new ( Arc :: new ( AtomicU32RequestIdProvider :: default ( ) ) , None ) ;
1676+ let send = peer. send_request_with_option ( ping ( ) , PeerRequestOptions :: no_options ( ) ) ;
1677+ let _handle = match scope {
1678+ Some ( id) => ORIGINATING_REQUEST . scope ( id, send) . await . unwrap ( ) ,
1679+ None => send. await . unwrap ( ) ,
1680+ } ;
1681+ let PeerSinkMessage :: Request { request, .. } = rx. recv ( ) . await . expect ( "sink message" )
1682+ else {
1683+ panic ! ( "expected a request sink message" ) ;
1684+ } ;
1685+ request
1686+ }
1687+
1688+ #[ tokio:: test]
1689+ async fn outbound_request_carries_originating_id_when_in_scope ( ) {
1690+ let request = send_and_capture ( Some ( RequestId :: Number ( 7 ) ) ) . await ;
1691+ let marker = request
1692+ . extensions ( )
1693+ . get :: < OriginatingRequestId > ( )
1694+ . expect ( "marker attached" ) ;
1695+ assert_eq ! ( marker. 0 , RequestId :: Number ( 7 ) ) ;
1696+ }
1697+
1698+ #[ tokio:: test]
1699+ async fn outbound_request_has_no_marker_outside_scope ( ) {
1700+ let request = send_and_capture ( None ) . await ;
1701+ assert ! ( request. extensions( ) . get:: <OriginatingRequestId >( ) . is_none( ) ) ;
1702+ }
1703+ }
0 commit comments