@@ -461,6 +461,25 @@ def test_selects_next_available_node_on_timeout(
461461 assert len (respx .calls ) == 3
462462
463463
464+ def test_client_errors_do_not_mark_nodes_unhealthy (
465+ fake_api_call : ApiCall ,
466+ mocker : MockerFixture ,
467+ ) -> None :
468+ """Pool exhaustion is local to the client and must not trigger failover."""
469+ node = fake_api_call .node_manager .get_node ()
470+ make_request = mocker .patch .object (
471+ fake_api_call .request_handler ,
472+ "make_request" ,
473+ side_effect = httpx .PoolTimeout ("No connection available" ),
474+ )
475+
476+ with pytest .raises (httpx .PoolTimeout ):
477+ fake_api_call .get ("/test" , as_json = True , entity_type = typing .Dict [str , str ])
478+
479+ assert node .healthy is True
480+ make_request .assert_called_once ()
481+
482+
464483def test_get_node_no_healthy_nodes (
465484 fake_api_call : ApiCall ,
466485 mocker : MockFixture ,
@@ -665,3 +684,61 @@ async def test_async_sleeps_retry_interval_between_retries(
665684 assert sleep_call == mocker .call (
666685 fake_async_api_call .config .retry_interval_seconds ,
667686 )
687+
688+
689+ @pytest .mark .parametrize (
690+ "client_side_error" ,
691+ [
692+ httpx .PoolTimeout ("Pool timeout" ),
693+ httpx .LocalProtocolError ("Local protocol error" ),
694+ httpx .DecodingError ("Decoding error" ),
695+ httpx .TooManyRedirects ("Too many redirects" ),
696+ ],
697+ )
698+ def test_client_side_error_does_not_mark_node_unhealthy (
699+ fake_api_call : ApiCall ,
700+ client_side_error : httpx .HTTPError ,
701+ ) -> None :
702+ """Test that client-side httpx errors propagate without failing over."""
703+ with respx .mock :
704+ respx .get ("http://nearest:8108/" ).mock (side_effect = client_side_error )
705+ node0_route = respx .get ("http://node0:8108/" ).mock (
706+ return_value = httpx .Response (200 , json = {"key" : "value" }),
707+ )
708+
709+ with pytest .raises (type (client_side_error )):
710+ fake_api_call .get ("/" , entity_type = typing .Dict [str , str ])
711+
712+ assert len (respx .calls ) == 1
713+ assert not node0_route .called
714+
715+ assert fake_api_call .config .nearest_node .healthy is True
716+
717+
718+ @pytest .mark .parametrize (
719+ "client_side_error" ,
720+ [
721+ httpx .PoolTimeout ("Pool timeout" ),
722+ httpx .LocalProtocolError ("Local protocol error" ),
723+ httpx .DecodingError ("Decoding error" ),
724+ httpx .TooManyRedirects ("Too many redirects" ),
725+ ],
726+ )
727+ async def test_async_client_side_error_does_not_mark_node_unhealthy (
728+ fake_async_api_call : AsyncApiCall ,
729+ client_side_error : httpx .HTTPError ,
730+ ) -> None :
731+ """Test that client-side httpx errors propagate without failing over (async)."""
732+ with respx .mock :
733+ respx .get ("http://nearest:8108/" ).mock (side_effect = client_side_error )
734+ node0_route = respx .get ("http://node0:8108/" ).mock (
735+ return_value = httpx .Response (200 , json = {"key" : "value" }),
736+ )
737+
738+ with pytest .raises (type (client_side_error )):
739+ await fake_async_api_call .get ("/" , entity_type = typing .Dict [str , str ])
740+
741+ assert len (respx .calls ) == 1
742+ assert not node0_route .called
743+
744+ assert fake_async_api_call .config .nearest_node .healthy is True
0 commit comments