diff --git a/src/testcontainers/core/wait_strategies.py b/src/testcontainers/core/wait_strategies.py index ac83ffa29..4e9f5a778 100644 --- a/src/testcontainers/core/wait_strategies.py +++ b/src/testcontainers/core/wait_strategies.py @@ -418,7 +418,12 @@ def _try_http_request(self, url: str, headers: dict[str, str], ssl_context: Any) with urlopen(request, timeout=1, context=ssl_context) as response: return self._check_response(response, url) - except (URLError, HTTPError) as e: + except HTTPError as e: + # HTTPError wraps the response file object, so it doubles as a + # context manager to avoid leaking the file descriptor. + with e: + return self._handle_http_error(e) + except URLError as e: return self._handle_http_error(e) except (ConnectionResetError, ConnectionRefusedError, BrokenPipeError, OSError) as e: # Handle connection-level errors that can occur during HTTP requests diff --git a/tests/core/test_wait_strategies.py b/tests/core/test_wait_strategies.py index 2f5f3d408..60c1be572 100644 --- a/tests/core/test_wait_strategies.py +++ b/tests/core/test_wait_strategies.py @@ -1,9 +1,12 @@ +import io import itertools import logging import re import time from datetime import timedelta +from email.message import Message from unittest.mock import Mock, patch +from urllib.error import HTTPError import pytest @@ -361,6 +364,31 @@ def test_from_url(self, url, expected_port, expected_path, expected_tls): assert strategy._path == expected_path assert strategy._tls is expected_tls + @pytest.mark.parametrize( + "status_codes,expected_result", + [ + ({503}, True), + (set(), False), + ], + ids=[ + "accepted_error_status_code", + "unaccepted_error_status_code", + ], + ) + @patch("testcontainers.core.wait_strategies.urlopen") + def test_try_http_request_closes_http_error(self, mock_urlopen, status_codes, expected_result): + """HTTPError holds the response's file object and must be closed (issue #1115).""" + fp = io.BytesIO(b"error body") + mock_urlopen.side_effect = HTTPError("http://localhost:8080/", 503, "Service Unavailable", Message(), fp) + strategy = HttpWaitStrategy(8080) + for code in status_codes: + strategy.for_status_code(code) + + result = strategy._try_http_request("http://localhost:8080/", {}, None) + + assert result is expected_result + assert fp.closed + class TestHealthcheckWaitStrategy: """Test the HealthcheckWaitStrategy class."""