@@ -775,6 +775,76 @@ async def test_shutdown_during_idle(http_protocol_cls: type[HTTPProtocol]):
775775 assert protocol .transport .is_closing ()
776776
777777
778+ async def test_shutdown_during_streaming_sends_disconnect (http_protocol_cls : type [HTTPProtocol ]):
779+ """When the server shuts down during an SSE/streaming response,
780+ receive() should return http.disconnect so the ASGI app can stop."""
781+ got_disconnect_event = False
782+
783+ async def app (scope : Scope , receive : ASGIReceiveCallable , send : ASGISendCallable ):
784+ nonlocal got_disconnect_event
785+
786+ await send (
787+ {
788+ "type" : "http.response.start" ,
789+ "status" : 200 ,
790+ "headers" : [(b"content-type" , b"text/event-stream" )],
791+ }
792+ )
793+ await send ({"type" : "http.response.body" , "body" : b"data: hello\n \n " , "more_body" : True })
794+
795+ # This simulates an SSE app waiting for disconnect
796+ message = await receive ()
797+ if message ["type" ] == "http.disconnect" :
798+ got_disconnect_event = True
799+
800+ protocol = get_connected_protocol (app , http_protocol_cls )
801+ protocol .data_received (SIMPLE_GET_REQUEST )
802+ # Trigger server shutdown while the app is streaming
803+ protocol .shutdown () # type: ignore[attr-defined]
804+ await protocol .loop .run_one ()
805+ assert got_disconnect_event
806+ assert b"HTTP/1.1 200 OK" in protocol .transport .buffer
807+ assert b"data: hello" in protocol .transport .buffer
808+ assert protocol .transport .is_closing ()
809+
810+
811+ async def test_shutdown_during_streaming_allows_send_before_exit (http_protocol_cls : type [HTTPProtocol ]):
812+ """During server shutdown, the app should still be able to send() data
813+ (e.g., a farewell SSE event) before returning."""
814+ farewell_sent = False
815+
816+ async def app (scope : Scope , receive : ASGIReceiveCallable , send : ASGISendCallable ):
817+ nonlocal farewell_sent
818+
819+ await send (
820+ {
821+ "type" : "http.response.start" ,
822+ "status" : 200 ,
823+ "headers" : [
824+ (b"content-type" , b"text/event-stream" ),
825+ (b"transfer-encoding" , b"chunked" ),
826+ ],
827+ }
828+ )
829+ await send ({"type" : "http.response.body" , "body" : b"data: hello\n \n " , "more_body" : True })
830+
831+ # Wait for disconnect
832+ message = await receive ()
833+ assert message ["type" ] == "http.disconnect"
834+
835+ # Send a farewell event — this should still work since the transport is open
836+ await send ({"type" : "http.response.body" , "body" : b"data: goodbye\n \n " , "more_body" : True })
837+ farewell_sent = True
838+
839+ protocol = get_connected_protocol (app , http_protocol_cls )
840+ protocol .data_received (SIMPLE_GET_REQUEST )
841+ protocol .shutdown () # type: ignore[attr-defined]
842+ await protocol .loop .run_one ()
843+ assert farewell_sent
844+ assert b"data: hello" in protocol .transport .buffer
845+ assert b"data: goodbye" in protocol .transport .buffer
846+
847+
778848async def test_100_continue_sent_when_body_consumed (http_protocol_cls : type [HTTPProtocol ]):
779849 async def app (scope : Scope , receive : ASGIReceiveCallable , send : ASGISendCallable ):
780850 body = b""
0 commit comments