@@ -59,6 +59,10 @@ hackney_conn_integration_test_() ->
5959 % % Async tests
6060 {" async request continuous" , {timeout , 30 , fun test_async_continuous /0 }},
6161 {" async request once mode" , {timeout , 30 , fun test_async_once /0 }},
62+ % % Mid-stream ownership reassignment
63+ {" set_owner while receiving body" , {timeout , 30 , fun test_set_owner_while_receiving /0 }},
64+ {" set_owner while streaming async" , {timeout , 30 , fun test_set_owner_while_streaming /0 }},
65+ {" set_owner API unchanged in other states" , {timeout , 30 , fun test_set_owner_api_unchanged /0 }},
6266 % % 1XX response handling
6367 {" skip 1XX informational responses" , {timeout , 30 , fun test_skip_1xx_responses /0 }}
6468 ]}.
@@ -632,6 +636,146 @@ test_async_once() ->
632636
633637 hackney_conn :stop (Pid ).
634638
639+ test_set_owner_while_receiving () ->
640+ % % HTTP/1.1: a short-lived worker starts the response and reads one chunk,
641+ % % then a third process reassigns ownership to a long-lived reader. The
642+ % % original owner exits and the reader drains the body to done, proving the
643+ % % connection is no longer torn down by the original owner's DOWN.
644+ Size = 200000 ,
645+ Opts = #{
646+ host => " 127.0.0.1" ,
647+ port => ? PORT ,
648+ transport => hackney_tcp ,
649+ connect_timeout => 5000 ,
650+ recv_timeout => 5000
651+ },
652+ {ok , Pid } = hackney_conn :start_link (Opts ),
653+ ok = hackney_conn :connect (Pid ),
654+
655+ Parent = self (),
656+ Path = <<" /chunked/" , (integer_to_binary (Size ))/binary >>,
657+
658+ {Worker , WMon } = spawn_monitor (fun () ->
659+ receive go -> ok end ,
660+ {ok , 200 , _ } = hackney_conn :request (Pid , <<" GET" >>, Path , [], <<>>),
661+ {ok , C1 } = hackney_conn :stream_body (Pid ),
662+ Parent ! {chunk1 , self (), C1 },
663+ receive stop -> ok end
664+ end ),
665+
666+ % % Tie the connection lifecycle to the worker (connected state).
667+ ok = hackney_conn :set_owner (Pid , Worker ),
668+ Worker ! go ,
669+
670+ FirstChunk = receive
671+ {chunk1 , Worker , C } -> C
672+ after 5000 -> error (chunk1_timeout )
673+ end ,
674+ ? assert (byte_size (FirstChunk ) > 0 ),
675+
676+ % % Reassign ownership mid-stream (receiving state) to a long-lived reader
677+ % % that stays alive as owner until we have verified.
678+ Reader = spawn (fun () ->
679+ Rest = stream_all (Pid , <<>>),
680+ Parent ! {rest , self (), Rest },
681+ receive stop -> ok end
682+ end ),
683+ ? assertEqual (ok , hackney_conn :set_owner (Pid , Reader )),
684+
685+ % % Original owner exits; the connection must survive.
686+ Worker ! stop ,
687+ receive {'DOWN' , WMon , process , Worker , _ } -> ok after 5000 -> error (worker_down_timeout ) end ,
688+ ? assert (is_process_alive (Pid )),
689+
690+ Rest = receive
691+ {rest , Reader , R } -> R
692+ after 10000 -> error (rest_timeout )
693+ end ,
694+
695+ ? assertEqual (Size , byte_size (FirstChunk ) + byte_size (Rest )),
696+ Reader ! stop ,
697+ hackney_conn :stop (Pid ).
698+
699+ test_set_owner_while_streaming () ->
700+ % % Async continuous: the request runs with the worker as lifecycle owner and
701+ % % a separate collector as stream_to. Mid-stream (streaming state) a third
702+ % % process reassigns ownership away from the worker, which then exits without
703+ % % stopping the connection. stream_to is unchanged, so the collector still
704+ % % receives every message through done.
705+ Size = 2000000 ,
706+ Opts = #{
707+ host => " 127.0.0.1" ,
708+ port => ? PORT ,
709+ transport => hackney_tcp ,
710+ connect_timeout => 5000 ,
711+ recv_timeout => 5000
712+ },
713+ {ok , Pid } = hackney_conn :start_link (Opts ),
714+ ok = hackney_conn :connect (Pid ),
715+
716+ Parent = self (),
717+ Path = <<" /chunked/" , (integer_to_binary (Size ))/binary >>,
718+
719+ {Owner , OMon } = spawn_monitor (fun () -> receive stop -> ok end end ),
720+ % % Owner becomes the lifecycle owner (connected state).
721+ ok = hackney_conn :set_owner (Pid , Owner ),
722+
723+ % % Collector issues the async request as its own caller, so stream_to is the
724+ % % collector while owner stays Owner (do_request_async leaves owner unchanged
725+ % % when StreamTo == caller).
726+ Collector = spawn (fun () ->
727+ {ok , Ref } = hackney_conn :request_async (Pid , <<" GET" >>, Path , [], <<>>, true ),
728+ Parent ! {started , self ()},
729+ Msgs = receive_all_async (Ref , []),
730+ Parent ! {collected , self (), Msgs }
731+ end ),
732+
733+ % % Wait until the request is issued (streaming has begun), then reassign
734+ % % ownership to the long-lived parent while the body is still draining.
735+ receive {started , Collector } -> ok after 5000 -> error (started_timeout ) end ,
736+ ? assertEqual (ok , hackney_conn :set_owner (Pid , self ())),
737+
738+ % % Original owner exits; the connection must survive.
739+ Owner ! stop ,
740+ receive {'DOWN' , OMon , process , Owner , _ } -> ok after 5000 -> error (owner_down_timeout ) end ,
741+ ? assert (is_process_alive (Pid )),
742+
743+ Msgs = receive
744+ {collected , Collector , M } -> M
745+ after 15000 -> error (collect_timeout )
746+ end ,
747+ ? assert (lists :member (done , Msgs )),
748+ ? assertEqual (Size , iolist_size ([B || B <- Msgs , is_binary (B )])),
749+ hackney_conn :stop (Pid ).
750+
751+ test_set_owner_api_unchanged () ->
752+ % % Regression: adding receiving/streaming handlers must not change the API
753+ % % elsewhere. set_owner still succeeds in connected and is still rejected
754+ % % with invalid_state in an untouched state (streaming_once / async once).
755+ Opts = #{
756+ host => " 127.0.0.1" ,
757+ port => ? PORT ,
758+ transport => hackney_tcp ,
759+ connect_timeout => 5000 ,
760+ recv_timeout => 5000
761+ },
762+ {ok , Pid } = hackney_conn :start_link (Opts ),
763+ ok = hackney_conn :connect (Pid ),
764+
765+ % % connected: reassignment still works, then hand ownership back to us.
766+ Target = spawn (fun () -> receive stop -> ok end end ),
767+ ? assertEqual (ok , hackney_conn :set_owner (Pid , Target )),
768+ ? assertEqual (ok , hackney_conn :set_owner (Pid , self ())),
769+ Target ! stop ,
770+
771+ % % streaming_once (async once, awaiting stream_next): still rejected.
772+ {ok , Ref } = hackney_conn :request_async (Pid , <<" GET" >>, <<" /get" >>, [], <<>>, once ),
773+ receive {hackney_response , Ref , {status , _ , _ }} -> ok after 5000 -> error (status_timeout ) end ,
774+ receive {hackney_response , Ref , {headers , _ }} -> ok after 5000 -> error (headers_timeout ) end ,
775+ ? assertEqual ({error , invalid_state }, hackney_conn :set_owner (Pid , self ())),
776+
777+ hackney_conn :stop (Pid ).
778+
635779% %====================================================================
636780% % 1XX Response Tests
637781% %====================================================================
0 commit comments