@@ -98,10 +98,21 @@ impl Recorder {
9898 . collect ( )
9999 }
100100
101+ fn delete_requests ( & self ) -> Vec < Call > {
102+ self . calls ( )
103+ . into_iter ( )
104+ . filter ( |( http_method, ..) | http_method == "DELETE" )
105+ . collect ( )
106+ }
107+
108+ /// Session headers on everything after the handshake that is not the teardown DELETE,
109+ /// which carries the id on purpose so the server tears the session down.
101110 fn session_headers_after_handshake ( & self ) -> Vec < Option < String > > {
102111 self . calls ( )
103112 . into_iter ( )
104- . filter ( |( _, jsonrpc_method, _) | jsonrpc_method != "initialize" )
113+ . filter ( |( http_method, jsonrpc_method, _) | {
114+ jsonrpc_method != "initialize" && http_method != "DELETE"
115+ } )
105116 . map ( |( .., session) | session)
106117 . collect ( )
107118 }
@@ -144,6 +155,19 @@ async fn handler(
144155 . expect ( "build GET rejection" ) ;
145156 }
146157
158+ // The shutdown DELETE carries no body, so record it before anything parses one.
159+ if method == axum:: http:: Method :: DELETE {
160+ state. seen . lock ( ) . expect ( "recorder poisoned" ) . push ( (
161+ "DELETE" . to_owned ( ) ,
162+ "-" . to_owned ( ) ,
163+ session,
164+ ) ) ;
165+ return Response :: builder ( )
166+ . status ( StatusCode :: OK )
167+ . body ( Body :: empty ( ) )
168+ . expect ( "build session teardown response" ) ;
169+ }
170+
147171 let request: serde_json:: Value = serde_json:: from_slice ( & body) . expect ( "valid JSON-RPC body" ) ;
148172 let jsonrpc_method = request[ "method" ] . as_str ( ) . unwrap_or ( "-" ) . to_owned ( ) ;
149173 state. seen . lock ( ) . expect ( "recorder poisoned" ) . push ( (
@@ -347,7 +371,7 @@ async fn replacement_handshake_at_a_modern_version_drops_the_session_too() {
347371 // Nor on anything after it, and no stream on the replacement session either.
348372 let echoed: Vec < _ > = replacement
349373 . iter ( )
350- . filter ( |( .. , session) | session. is_some ( ) )
374+ . filter ( |( http_method , _ , session) | session. is_some ( ) && http_method != "DELETE" )
351375 . collect ( ) ;
352376 assert ! (
353377 echoed. is_empty( ) ,
@@ -359,6 +383,16 @@ async fn replacement_handshake_at_a_modern_version_drops_the_session_too() {
359383 "expected only the legacy session's GET stream, got {:?}" ,
360384 recorder. get_requests( )
361385 ) ;
386+
387+ // Dropping the id is a request-and-stream decision, not a licence to leak server
388+ // state: the session the server really did create is still torn down at shutdown.
389+ let deletes = recorder. delete_requests ( ) ;
390+ assert ! (
391+ deletes
392+ . iter( )
393+ . any( |( .., session) | session. as_deref( ) == Some ( REPLACEMENT_SESSION_ID ) ) ,
394+ "the replacement session was never deleted at shutdown: {deletes:?}"
395+ ) ;
362396}
363397
364398#[ tokio:: test]
0 commit comments