Skip to content

Commit 120c121

Browse files
committed
fix: address comments
1 parent 175158a commit 120c121

3 files changed

Lines changed: 138 additions & 2 deletions

File tree

openfga_sdk/sync/api_client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ def __exit__(self, exc_type, exc_value, traceback):
109109
self.close()
110110

111111
def close(self):
112+
self.rest_client.close()
112113
if self._pool:
113114
self._pool.close()
114115
self._pool.join()

openfga_sdk/sync/rest.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -493,7 +493,13 @@ def request(
493493
# Log the response body
494494
logger.debug("response body: %s", wrapped_response.data.decode("utf-8"))
495495

496-
# Handle any errors that may have occurred
497-
self.handle_response_exception(raw_response)
496+
# Handle any errors that may have occurred. If an exception is raised,
497+
# ensure the underlying response is closed so the connection is not
498+
# leaked from the pool.
499+
try:
500+
self.handle_response_exception(raw_response)
501+
except Exception:
502+
raw_response.close()
503+
raise
498504

499505
return wrapped_response or raw_response

test/sync/rest_test.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -731,3 +731,132 @@ def test_ssl_context_with_all_ssl_options(mock_pool_manager, mock_create_context
731731
call_kwargs = mock_pool_manager.call_args[1]
732732
assert call_kwargs["ssl_context"] == mock_ssl_context
733733
assert call_kwargs["maxsize"] == 8
734+
735+
736+
def test_request_does_not_clear_pool_after_request():
737+
"""Ensure request() does not call close()/pool_manager.clear() after each request,
738+
so pooled connections are preserved for reuse."""
739+
mock_config = MagicMock(
740+
spec=[
741+
"verify_ssl",
742+
"ssl_ca_cert",
743+
"cert_file",
744+
"key_file",
745+
"assert_hostname",
746+
"retries",
747+
"socket_options",
748+
"connection_pool_maxsize",
749+
"timeout_millisec",
750+
"proxy",
751+
"proxy_headers",
752+
]
753+
)
754+
mock_config.ssl_ca_cert = None
755+
mock_config.cert_file = None
756+
mock_config.key_file = None
757+
mock_config.verify_ssl = True
758+
mock_config.connection_pool_maxsize = 4
759+
mock_config.timeout_millisec = 5000
760+
mock_config.proxy = None
761+
mock_config.proxy_headers = None
762+
763+
client = RESTClientObject(configuration=mock_config)
764+
mock_pool_manager = MagicMock()
765+
client.pool_manager = mock_pool_manager
766+
767+
mock_raw_response = MagicMock()
768+
mock_raw_response.status = 200
769+
mock_raw_response.reason = "OK"
770+
mock_raw_response.data = b'{"ok":true}'
771+
772+
mock_pool_manager.request.return_value = mock_raw_response
773+
774+
# Make multiple requests
775+
client.request(method="GET", url="http://example.com", _preload_content=True)
776+
client.request(method="GET", url="http://example.com", _preload_content=True)
777+
778+
# pool_manager.clear() should never have been called
779+
mock_pool_manager.clear.assert_not_called()
780+
781+
782+
def test_request_closes_response_on_error():
783+
"""Ensure that if handle_response_exception raises, the raw response is closed
784+
so the connection is not leaked from the pool."""
785+
mock_config = MagicMock(
786+
spec=[
787+
"verify_ssl",
788+
"ssl_ca_cert",
789+
"cert_file",
790+
"key_file",
791+
"assert_hostname",
792+
"retries",
793+
"socket_options",
794+
"connection_pool_maxsize",
795+
"timeout_millisec",
796+
"proxy",
797+
"proxy_headers",
798+
]
799+
)
800+
mock_config.ssl_ca_cert = None
801+
mock_config.cert_file = None
802+
mock_config.key_file = None
803+
mock_config.verify_ssl = True
804+
mock_config.connection_pool_maxsize = 4
805+
mock_config.timeout_millisec = 5000
806+
mock_config.proxy = None
807+
mock_config.proxy_headers = None
808+
809+
client = RESTClientObject(configuration=mock_config)
810+
mock_pool_manager = MagicMock()
811+
client.pool_manager = mock_pool_manager
812+
813+
mock_raw_response = MagicMock()
814+
mock_raw_response.status = 500
815+
mock_raw_response.reason = "Internal Server Error"
816+
mock_raw_response.data = b'{"error":"something went wrong"}'
817+
mock_raw_response.getheaders.return_value = {}
818+
819+
mock_pool_manager.request.return_value = mock_raw_response
820+
821+
with pytest.raises(ServiceException):
822+
client.request(method="GET", url="http://example.com", _preload_content=True)
823+
824+
# Verify raw_response.close() was called to release the connection
825+
mock_raw_response.close.assert_called_once()
826+
827+
828+
def test_api_client_close_calls_rest_client_close():
829+
"""Ensure the sync ApiClient.close() delegates to rest_client.close()
830+
so pooled connections are cleaned up at shutdown."""
831+
from openfga_sdk.configuration import Configuration
832+
from openfga_sdk.sync.api_client import ApiClient
833+
834+
configuration = Configuration(
835+
api_url="http://api.fga.example",
836+
)
837+
838+
api_client = ApiClient(configuration)
839+
mock_rest_client = MagicMock()
840+
api_client.rest_client = mock_rest_client
841+
842+
api_client.close()
843+
844+
mock_rest_client.close.assert_called_once()
845+
846+
847+
def test_api_client_context_manager_calls_close():
848+
"""Ensure the sync ApiClient context manager calls close() on exit,
849+
which in turn cleans up the REST client's connection pool."""
850+
from openfga_sdk.configuration import Configuration
851+
from openfga_sdk.sync.api_client import ApiClient
852+
853+
configuration = Configuration(
854+
api_url="http://api.fga.example",
855+
)
856+
857+
with ApiClient(configuration) as api_client:
858+
mock_rest_client = MagicMock()
859+
api_client.rest_client = mock_rest_client
860+
861+
# After exiting the context manager, close() should have been called
862+
mock_rest_client.close.assert_called_once()

0 commit comments

Comments
 (0)