@@ -2008,3 +2008,101 @@ async fn test_binary_octet_stream_content_type() {
20082008 "Expected application/octet-stream content-type for binary, got: {response}"
20092009 ) ;
20102010}
2011+
2012+ #[ tokio:: test]
2013+ async fn test_sse_cancelled_on_hot_reload ( ) {
2014+ use tokio:: io:: { AsyncBufReadExt , AsyncWriteExt , BufReader } ;
2015+
2016+ // Spawn server with an SSE endpoint that streams indefinitely
2017+ let ( mut child, mut stdin, addr_rx) = TestServerWithStdin :: spawn ( "127.0.0.1:0" , false ) ;
2018+
2019+ // Send initial SSE script - stream many events slowly
2020+ let sse_script = r#"{|req|
2021+ .response {headers: {"Content-Type": "text/event-stream"}}
2022+ 1..100 | each {|i|
2023+ sleep 100ms
2024+ $"data: event-($i)\n\n"
2025+ }
2026+ }"# ;
2027+ stdin. write_all ( sse_script. as_bytes ( ) ) . await . unwrap ( ) ;
2028+ stdin. write_all ( b"\0 " ) . await . unwrap ( ) ;
2029+ stdin. flush ( ) . await . unwrap ( ) ;
2030+
2031+ // Wait for server to start
2032+ let address = tokio:: time:: timeout ( std:: time:: Duration :: from_secs ( 5 ) , addr_rx)
2033+ . await
2034+ . expect ( "Server didn't start" )
2035+ . expect ( "Channel closed" ) ;
2036+
2037+ tokio:: time:: sleep ( std:: time:: Duration :: from_millis ( 200 ) ) . await ;
2038+
2039+ // Start curl to SSE endpoint
2040+ let mut sse_child = tokio:: process:: Command :: new ( "curl" )
2041+ . arg ( "-sN" )
2042+ . arg ( & address)
2043+ . stdout ( std:: process:: Stdio :: piped ( ) )
2044+ . spawn ( )
2045+ . expect ( "Failed to start curl" ) ;
2046+
2047+ let stdout = sse_child. stdout . take ( ) . expect ( "Failed to get stdout" ) ;
2048+ let mut reader = BufReader :: new ( stdout) . lines ( ) ;
2049+
2050+ // Read a few events to confirm SSE is working
2051+ let mut events_received = 0 ;
2052+ for _ in 0 ..3 {
2053+ if let Ok ( Ok ( Some ( line) ) ) =
2054+ tokio:: time:: timeout ( std:: time:: Duration :: from_secs ( 2 ) , reader. next_line ( ) ) . await
2055+ {
2056+ if line. starts_with ( "data:" ) {
2057+ events_received += 1 ;
2058+ }
2059+ }
2060+ }
2061+ assert ! (
2062+ events_received >= 1 ,
2063+ "Should have received at least one SSE event before reload"
2064+ ) ;
2065+
2066+ // Trigger hot reload with a different script
2067+ let new_script = r#"{|req| "reloaded"}"# ;
2068+ stdin. write_all ( new_script. as_bytes ( ) ) . await . unwrap ( ) ;
2069+ stdin. write_all ( b"\0 " ) . await . unwrap ( ) ;
2070+ stdin. flush ( ) . await . unwrap ( ) ;
2071+
2072+ // Wait for reload to process
2073+ tokio:: time:: sleep ( std:: time:: Duration :: from_millis ( 200 ) ) . await ;
2074+
2075+ // After reload, the SSE stream should be cancelled.
2076+ // With HTTP keep-alive, curl won't exit on its own, but no more events should arrive.
2077+ // Try to read another event - it should timeout (stream cancelled) or return None.
2078+ let more_events =
2079+ tokio:: time:: timeout ( std:: time:: Duration :: from_millis ( 500 ) , reader. next_line ( ) ) . await ;
2080+
2081+ // Either timeout (stream stalled) or None (stream ended) is acceptable
2082+ let stream_stopped = match more_events {
2083+ Err ( _) => true , // Timeout - no more data
2084+ Ok ( Ok ( None ) ) => true , // Stream ended
2085+ Ok ( Ok ( Some ( line) ) ) => !line. starts_with ( "data:" ) , // Got something but not an event
2086+ Ok ( Err ( _) ) => true , // Read error
2087+ } ;
2088+ assert ! ( stream_stopped, "SSE stream should stop after reload" ) ;
2089+
2090+ // Kill the curl process since it won't exit with keep-alive
2091+ sse_child. kill ( ) . await . ok ( ) ;
2092+
2093+ // Verify the new handler works
2094+ let output = std:: process:: Command :: new ( "curl" )
2095+ . arg ( "-s" )
2096+ . arg ( & address)
2097+ . output ( )
2098+ . expect ( "curl failed" ) ;
2099+
2100+ assert_eq ! (
2101+ String :: from_utf8_lossy( & output. stdout) . trim( ) ,
2102+ "reloaded" ,
2103+ "New handler should be active after reload"
2104+ ) ;
2105+
2106+ // Cleanup - kill the server
2107+ child. kill ( ) . await . ok ( ) ;
2108+ }
0 commit comments