Repository: aio-libs/aiohttp (2025-07-09 ~ 2025-07-09)

problem_statement: Regression when closing session
### Describe the bug

[V3.12.4](https://github.com/aio-libs/aiohttp/releases/tag/v3.12.4) Introduced a regression, in which, session with some proxies can't be closed.
Originally posted: https://github.com/aio-libs/aiohttp/discussions/11261

### To Reproduce

*Done* never prints here:
```python
import asyncio
from aiohttp import ClientSession

async def main():
    proxy = ""
    session = ClientSession(proxy=proxy)

    async with session.get(
        "https://api.ipify.org/",
    ) as response:
        ip = await response.text()
    print("Closing session...")
    await session.close()
    print("Done")
    return ip

asyncio.run(main())
```
Using `async with` not help.

### Expected behavior

Aiohttp closes session, without running forever

### Logs/tracebacks

**3.12.3**:
```
C:\Users\topsp\OneDrive\Desktop\project>pip install aiohttp==3.12.4
Collecting aiohttp==3.12.4
  Using cached aiohttp-3.12.4-cp313-cp313-win_amd64.whl.metadata (7.9 kB)
Requirement already satisfied: aiohappyeyeballs>=2.5.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.4) (2.6.1)
Requirement already satisfied: aiosignal>=1.1.2 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.4) (1.3.2)
Requirement already satisfied: attrs>=17.3.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.4) (25.3.0)
Requirement already satisfied: frozenlist>=1.1.1 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.4) (1.5.0)
Requirement already satisfied: multidict<7.0,>=4.5 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.4) (6.1.0)
Requirement already satisfied: propcache>=0.2.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.4) (0.2.1)
Requirement already satisfied: yarl<2.0,>=1.17.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.4) (1.18.3)
Requirement already satisfied: idna>=2.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from yarl<2.0,>=1.17.0->aiohttp==3.12.4) (3.10)
Using cached aiohttp-3.12.4-cp313-cp313-win_amd64.whl (439 kB)
Installing collected packages: aiohttp
  Attempting uninstall: aiohttp
    Found existing installation: aiohttp 3.12.3
    Uninstalling aiohttp-3.12.3:
      Successfully uninstalled aiohttp-3.12.3
Successfully installed aiohttp-3.12.4
 
C:\Users\topsp\OneDrive\Desktop\project>python script.py
Closing session...
Traceback (most recent call last):
  File "C:\Users\topsp\AppData\Local\Programs\Python\Python313\Lib\asyncio\runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "C:\Users\topsp\AppData\Local\Programs\Python\Python313\Lib\asyncio\base_events.py", line 725, in run_until_complete
    return future.result()
           ~~~~~~~~~~~~~^^
  File "C:\Users\topsp\OneDrive\Desktop\project\script.py", line 15, in main
    await connector.close()
  File "C:\Users\topsp\AppData\Local\Programs\Python\Python313\Lib\site-packages\aiohttp\connector.py", line 137, in _wait_for_close
    results = await asyncio.gather(*waiters, return_exceptions=True)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
asyncio.exceptions.CancelledError
 
During handling of the above exception, another exception occurred:
 
Traceback (most recent call last):
  File "C:\Users\topsp\OneDrive\Desktop\project\script.py", line 21, in <module>
    asyncio.run(main(), debug=True)
    ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\topsp\AppData\Local\Programs\Python\Python313\Lib\asyncio\runners.py", line 195, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
  File "C:\Users\topsp\AppData\Local\Programs\Python\Python313\Lib\asyncio\runners.py", line 123, in run
    raise KeyboardInterrupt()
KeyboardInterrupt
```
**3.12.4**:
```
C:\Users\topsp\OneDrive\Desktop\project>pip install aiohttp==3.12.3
Collecting aiohttp==3.12.3
  Using cached aiohttp-3.12.3-cp313-cp313-win_amd64.whl.metadata (7.9 kB)
Requirement already satisfied: aiohappyeyeballs>=2.5.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.3) (2.6.1)
Requirement already satisfied: aiosignal>=1.1.2 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.3) (1.3.2)
Requirement already satisfied: attrs>=17.3.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.3) (25.3.0)
Requirement already satisfied: frozenlist>=1.1.1 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.3) (1.5.0)
Requirement already satisfied: multidict<7.0,>=4.5 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.3) (6.1.0)
Requirement already satisfied: propcache>=0.2.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.3) (0.2.1)
Requirement already satisfied: yarl<2.0,>=1.17.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.3) (1.18.3)
Requirement already satisfied: idna>=2.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from yarl<2.0,>=1.17.0->aiohttp==3.12.3) (3.10)
Using cached aiohttp-3.12.3-cp313-cp313-win_amd64.whl (438 kB)
Installing collected packages: aiohttp
  Attempting uninstall: aiohttp
    Found existing installation: aiohttp 3.12.4
    Uninstalling aiohttp-3.12.4:
      Successfully uninstalled aiohttp-3.12.4
Successfully installed aiohttp-3.12.3
 
C:\Users\topsp\OneDrive\Desktop\project>python script.py
Closing session...
Done
```

### Python Version

```console
Python 3.13.5
```

### aiohttp Version

```console
aiohttp==3.12.13
```

### multidict Version

```console
multidict==6.6.3
```

### propcache Version

```console
propcache==0.3.2
```

### yarl Version

```console
yarl==1.20.1
```

### OS

Windows 10

### Related component

Client

### Additional context

Thank you for investigating this issue.

### Code of Conduct

- [x] I agree to follow the aio-libs Code of Conduct
date: 2025-07-09
patch: diff --git a/aiohttp/connector.py b/aiohttp/connector.py
index 4479ae32..0fbacde3 100644
--- a/aiohttp/connector.py
+++ b/aiohttp/connector.py
@@ -229,6 +229,26 @@ class Connection:
         return self._protocol is None or not self._protocol.is_connected()
 
 
+class _ConnectTunnelConnection(Connection):
+    """Special connection wrapper for CONNECT tunnels that must never be pooled.
+
+    This connection wraps the proxy connection that will be upgraded with TLS.
+    It must never be released to the pool because:
+    1. Its 'closed' future will never complete, causing session.close() to hang
+    2. It represents an intermediate state, not a reusable connection
+    3. The real connection (with TLS) will be created separately
+    """
+
+    def release(self) -> None:
+        """Do nothing - don't pool or close the connection.
+
+        These connections are an intermediate state during the CONNECT tunnel
+        setup and will be cleaned up naturally after the TLS upgrade. If they
+        were to be pooled, they would never be properly closed, causing
+        session.close() to wait forever for their 'closed' future.
+        """
+
+
 class _TransportPlaceholder:
     """placeholder for BaseConnector.connect function"""
 
@@ -1612,7 +1632,7 @@ class TCPConnector(BaseConnector):
             key = req.connection_key._replace(
                 proxy=None, proxy_auth=None, proxy_headers_hash=None
             )
-            conn = Connection(self, key, proto, self._loop)
+            conn = _ConnectTunnelConnection(self, key, proto, self._loop)
             proxy_resp = await proxy_req.send(conn)
             try:
                 protocol = conn._protocol

test_patch: diff --git a/tests/test_connector.py b/tests/test_connector.py
index c7938ed0..9932dee5 100644
--- a/tests/test_connector.py
+++ b/tests/test_connector.py
@@ -40,6 +40,7 @@ from aiohttp.connector import (
     AddrInfoType,
     Connection,
     TCPConnector,
+    _ConnectTunnelConnection,
     _DNSCacheTable,
 )
 from aiohttp.resolver import ResolveResult
@@ -4311,3 +4312,31 @@ async def test_available_connections_no_limits(
     connection1.close()
     assert conn._available_connections(key) == 1
     assert conn._available_connections(other_host_key2) == 1
+
+
+async def test_connect_tunnel_connection_release(
+    loop: asyncio.AbstractEventLoop,
+) -> None:
+    """Test _ConnectTunnelConnection.release() does not pool the connection."""
+    connector = mock.create_autospec(
+        aiohttp.BaseConnector, spec_set=True, instance=True
+    )
+    key = mock.create_autospec(ConnectionKey, spec_set=True, instance=True)
+    protocol = mock.create_autospec(ResponseHandler, spec_set=True, instance=True)
+
+    # Create a connect tunnel connection
+    conn = _ConnectTunnelConnection(connector, key, protocol, loop)
+
+    # Verify protocol is set
+    assert conn._protocol is protocol
+
+    # Release should do nothing (not pool the connection)
+    conn.release()
+
+    # Protocol should still be there (not released to pool)
+    assert conn._protocol is protocol
+    # Connector._release should NOT have been called
+    connector._release.assert_not_called()
+
+    # Clean up to avoid resource warning
+    conn.close()
diff --git a/tests/test_proxy_functional.py b/tests/test_proxy_functional.py
index 5b33ed6c..f4bc020d 100644
--- a/tests/test_proxy_functional.py
+++ b/tests/test_proxy_functional.py
@@ -4,6 +4,7 @@ import pathlib
 import platform
 import ssl
 import sys
+from contextlib import suppress
 from re import match as match_regex
 from typing import Awaitable, Callable
 from unittest import mock
@@ -17,6 +18,7 @@ import aiohttp
 from aiohttp import ClientResponse, web
 from aiohttp.client_exceptions import ClientConnectionError
 from aiohttp.helpers import IS_MACOS, IS_WINDOWS
+from aiohttp.pytest_plugin import AiohttpServer
 
 ASYNCIO_SUPPORTS_TLS_IN_TLS = sys.version_info >= (3, 11)
 
@@ -884,3 +886,46 @@ async def test_proxy_auth() -> None:
                 proxy_auth=("user", "pass"),
             ):
                 pass
+
+
+async def test_https_proxy_connect_tunnel_session_close_no_hang(
+    aiohttp_server: AiohttpServer,
+) -> None:
+    """Test that CONNECT tunnel connections are not pooled."""
+    # Regression test for issue #11273.
+
+    # Create a minimal proxy server
+    # The CONNECT method is handled at the protocol level, not by the handler
+    proxy_app = web.Application()
+    proxy_server = await aiohttp_server(proxy_app)
+    proxy_url = f"http://{proxy_server.host}:{proxy_server.port}"
+
+    # Create session and make HTTPS request through proxy
+    session = aiohttp.ClientSession()
+
+    try:
+        # This will fail during TLS upgrade because proxy doesn't establish tunnel
+        with suppress(aiohttp.ClientError):
+            async with session.get("https://example.com/test", proxy=proxy_url) as resp:
+                await resp.read()
+
+        # The critical test: Check if any connections were pooled with proxy=None
+        # This is the root cause of the hang - CONNECT tunnel connections
+        # should NOT be pooled
+        connector = session.connector
+        assert connector is not None
+
+        # Count connections with proxy=None in the pool
+        proxy_none_keys = [key for key in connector._conns if key.proxy is None]
+        proxy_none_count = len(proxy_none_keys)
+
+        # Before the fix, there would be a connection with proxy=None
+        # After the fix, CONNECT tunnel connections are not pooled
+        assert proxy_none_count == 0, (
+            f"Found {proxy_none_count} connections with proxy=None in pool. "
+            f"CONNECT tunnel connections should not be pooled - this is bug #11273"
+        )
+
+    finally:
+        # Clean close
+        await session.close()


----

problem_statement: Regression when closing session
### Describe the bug

[V3.12.4](https://github.com/aio-libs/aiohttp/releases/tag/v3.12.4) Introduced a regression, in which, session with some proxies can't be closed.
Originally posted: https://github.com/aio-libs/aiohttp/discussions/11261

### To Reproduce

*Done* never prints here:
```python
import asyncio
from aiohttp import ClientSession

async def main():
    proxy = ""
    session = ClientSession(proxy=proxy)

    async with session.get(
        "https://api.ipify.org/",
    ) as response:
        ip = await response.text()
    print("Closing session...")
    await session.close()
    print("Done")
    return ip

asyncio.run(main())
```
Using `async with` not help.

### Expected behavior

Aiohttp closes session, without running forever

### Logs/tracebacks

**3.12.3**:
```
C:\Users\topsp\OneDrive\Desktop\project>pip install aiohttp==3.12.4
Collecting aiohttp==3.12.4
  Using cached aiohttp-3.12.4-cp313-cp313-win_amd64.whl.metadata (7.9 kB)
Requirement already satisfied: aiohappyeyeballs>=2.5.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.4) (2.6.1)
Requirement already satisfied: aiosignal>=1.1.2 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.4) (1.3.2)
Requirement already satisfied: attrs>=17.3.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.4) (25.3.0)
Requirement already satisfied: frozenlist>=1.1.1 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.4) (1.5.0)
Requirement already satisfied: multidict<7.0,>=4.5 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.4) (6.1.0)
Requirement already satisfied: propcache>=0.2.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.4) (0.2.1)
Requirement already satisfied: yarl<2.0,>=1.17.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.4) (1.18.3)
Requirement already satisfied: idna>=2.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from yarl<2.0,>=1.17.0->aiohttp==3.12.4) (3.10)
Using cached aiohttp-3.12.4-cp313-cp313-win_amd64.whl (439 kB)
Installing collected packages: aiohttp
  Attempting uninstall: aiohttp
    Found existing installation: aiohttp 3.12.3
    Uninstalling aiohttp-3.12.3:
      Successfully uninstalled aiohttp-3.12.3
Successfully installed aiohttp-3.12.4
 
C:\Users\topsp\OneDrive\Desktop\project>python script.py
Closing session...
Traceback (most recent call last):
  File "C:\Users\topsp\AppData\Local\Programs\Python\Python313\Lib\asyncio\runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "C:\Users\topsp\AppData\Local\Programs\Python\Python313\Lib\asyncio\base_events.py", line 725, in run_until_complete
    return future.result()
           ~~~~~~~~~~~~~^^
  File "C:\Users\topsp\OneDrive\Desktop\project\script.py", line 15, in main
    await connector.close()
  File "C:\Users\topsp\AppData\Local\Programs\Python\Python313\Lib\site-packages\aiohttp\connector.py", line 137, in _wait_for_close
    results = await asyncio.gather(*waiters, return_exceptions=True)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
asyncio.exceptions.CancelledError
 
During handling of the above exception, another exception occurred:
 
Traceback (most recent call last):
  File "C:\Users\topsp\OneDrive\Desktop\project\script.py", line 21, in <module>
    asyncio.run(main(), debug=True)
    ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\topsp\AppData\Local\Programs\Python\Python313\Lib\asyncio\runners.py", line 195, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
  File "C:\Users\topsp\AppData\Local\Programs\Python\Python313\Lib\asyncio\runners.py", line 123, in run
    raise KeyboardInterrupt()
KeyboardInterrupt
```
**3.12.4**:
```
C:\Users\topsp\OneDrive\Desktop\project>pip install aiohttp==3.12.3
Collecting aiohttp==3.12.3
  Using cached aiohttp-3.12.3-cp313-cp313-win_amd64.whl.metadata (7.9 kB)
Requirement already satisfied: aiohappyeyeballs>=2.5.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.3) (2.6.1)
Requirement already satisfied: aiosignal>=1.1.2 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.3) (1.3.2)
Requirement already satisfied: attrs>=17.3.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.3) (25.3.0)
Requirement already satisfied: frozenlist>=1.1.1 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.3) (1.5.0)
Requirement already satisfied: multidict<7.0,>=4.5 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.3) (6.1.0)
Requirement already satisfied: propcache>=0.2.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.3) (0.2.1)
Requirement already satisfied: yarl<2.0,>=1.17.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.3) (1.18.3)
Requirement already satisfied: idna>=2.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from yarl<2.0,>=1.17.0->aiohttp==3.12.3) (3.10)
Using cached aiohttp-3.12.3-cp313-cp313-win_amd64.whl (438 kB)
Installing collected packages: aiohttp
  Attempting uninstall: aiohttp
    Found existing installation: aiohttp 3.12.4
    Uninstalling aiohttp-3.12.4:
      Successfully uninstalled aiohttp-3.12.4
Successfully installed aiohttp-3.12.3
 
C:\Users\topsp\OneDrive\Desktop\project>python script.py
Closing session...
Done
```

### Python Version

```console
Python 3.13.5
```

### aiohttp Version

```console
aiohttp==3.12.13
```

### multidict Version

```console
multidict==6.6.3
```

### propcache Version

```console
propcache==0.3.2
```

### yarl Version

```console
yarl==1.20.1
```

### OS

Windows 10

### Related component

Client

### Additional context

Thank you for investigating this issue.

### Code of Conduct

- [x] I agree to follow the aio-libs Code of Conduct
date: 2025-07-09
patch: diff --git a/CHANGES/11273.bugfix.rst b/CHANGES/11273.bugfix.rst
new file mode 100644
index 00000000..b4d9948f
--- /dev/null
+++ b/CHANGES/11273.bugfix.rst
@@ -0,0 +1 @@
+Fixed :py:meth:`ClientSession.close() <aiohttp.ClientSession.close>` hanging indefinitely when using HTTPS requests through HTTP proxies -- by :user:`bdraco`.

test_patch: diff --git a/tests/test_connector.py b/tests/test_connector.py
index f4f33f74c..90a321401 100644
--- a/tests/test_connector.py
+++ b/tests/test_connector.py
@@ -40,6 +40,7 @@ from aiohttp.connector import (
     AddrInfoType,
     Connection,
     TCPConnector,
+    _ConnectTunnelConnection,
     _DNSCacheTable,
 )
 from aiohttp.resolver import ResolveResult
@@ -4308,3 +4309,31 @@ async def test_available_connections_no_limits(
     connection1.close()
     assert conn._available_connections(key) == 1
     assert conn._available_connections(other_host_key2) == 1
+
+
+async def test_connect_tunnel_connection_release(
+    loop: asyncio.AbstractEventLoop,
+) -> None:
+    """Test _ConnectTunnelConnection.release() does not pool the connection."""
+    connector = mock.create_autospec(
+        aiohttp.BaseConnector, spec_set=True, instance=True
+    )
+    key = mock.create_autospec(ConnectionKey, spec_set=True, instance=True)
+    protocol = mock.create_autospec(ResponseHandler, spec_set=True, instance=True)
+
+    # Create a connect tunnel connection
+    conn = _ConnectTunnelConnection(connector, key, protocol, loop)
+
+    # Verify protocol is set
+    assert conn._protocol is protocol
+
+    # Release should do nothing (not pool the connection)
+    conn.release()
+
+    # Protocol should still be there (not released to pool)
+    assert conn._protocol is protocol
+    # Connector._release should NOT have been called
+    connector._release.assert_not_called()
+
+    # Clean up to avoid resource warning
+    conn.close()
diff --git a/tests/test_proxy_functional.py b/tests/test_proxy_functional.py
index 5b33ed6ca..f4bc020d1 100644
--- a/tests/test_proxy_functional.py
+++ b/tests/test_proxy_functional.py
@@ -4,6 +4,7 @@ import pathlib
 import platform
 import ssl
 import sys
+from contextlib import suppress
 from re import match as match_regex
 from typing import Awaitable, Callable
 from unittest import mock
@@ -17,6 +18,7 @@ import aiohttp
 from aiohttp import ClientResponse, web
 from aiohttp.client_exceptions import ClientConnectionError
 from aiohttp.helpers import IS_MACOS, IS_WINDOWS
+from aiohttp.pytest_plugin import AiohttpServer
 
 ASYNCIO_SUPPORTS_TLS_IN_TLS = sys.version_info >= (3, 11)
 
@@ -884,3 +886,46 @@ async def test_proxy_auth() -> None:
                 proxy_auth=("user", "pass"),
             ):
                 pass
+
+
+async def test_https_proxy_connect_tunnel_session_close_no_hang(
+    aiohttp_server: AiohttpServer,
+) -> None:
+    """Test that CONNECT tunnel connections are not pooled."""
+    # Regression test for issue #11273.
+
+    # Create a minimal proxy server
+    # The CONNECT method is handled at the protocol level, not by the handler
+    proxy_app = web.Application()
+    proxy_server = await aiohttp_server(proxy_app)
+    proxy_url = f"http://{proxy_server.host}:{proxy_server.port}"
+
+    # Create session and make HTTPS request through proxy
+    session = aiohttp.ClientSession()
+
+    try:
+        # This will fail during TLS upgrade because proxy doesn't establish tunnel
+        with suppress(aiohttp.ClientError):
+            async with session.get("https://example.com/test", proxy=proxy_url) as resp:
+                await resp.read()
+
+        # The critical test: Check if any connections were pooled with proxy=None
+        # This is the root cause of the hang - CONNECT tunnel connections
+        # should NOT be pooled
+        connector = session.connector
+        assert connector is not None
+
+        # Count connections with proxy=None in the pool
+        proxy_none_keys = [key for key in connector._conns if key.proxy is None]
+        proxy_none_count = len(proxy_none_keys)
+
+        # Before the fix, there would be a connection with proxy=None
+        # After the fix, CONNECT tunnel connections are not pooled
+        assert proxy_none_count == 0, (
+            f"Found {proxy_none_count} connections with proxy=None in pool. "
+            f"CONNECT tunnel connections should not be pooled - this is bug #11273"
+        )
+
+    finally:
+        # Clean close
+        await session.close()


----

problem_statement: Regression when closing session
### Describe the bug

[V3.12.4](https://github.com/aio-libs/aiohttp/releases/tag/v3.12.4) Introduced a regression, in which, session with some proxies can't be closed.
Originally posted: https://github.com/aio-libs/aiohttp/discussions/11261

### To Reproduce

*Done* never prints here:
```python
import asyncio
from aiohttp import ClientSession

async def main():
    proxy = ""
    session = ClientSession(proxy=proxy)

    async with session.get(
        "https://api.ipify.org/",
    ) as response:
        ip = await response.text()
    print("Closing session...")
    await session.close()
    print("Done")
    return ip

asyncio.run(main())
```
Using `async with` not help.

### Expected behavior

Aiohttp closes session, without running forever

### Logs/tracebacks

**3.12.3**:
```
C:\Users\topsp\OneDrive\Desktop\project>pip install aiohttp==3.12.4
Collecting aiohttp==3.12.4
  Using cached aiohttp-3.12.4-cp313-cp313-win_amd64.whl.metadata (7.9 kB)
Requirement already satisfied: aiohappyeyeballs>=2.5.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.4) (2.6.1)
Requirement already satisfied: aiosignal>=1.1.2 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.4) (1.3.2)
Requirement already satisfied: attrs>=17.3.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.4) (25.3.0)
Requirement already satisfied: frozenlist>=1.1.1 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.4) (1.5.0)
Requirement already satisfied: multidict<7.0,>=4.5 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.4) (6.1.0)
Requirement already satisfied: propcache>=0.2.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.4) (0.2.1)
Requirement already satisfied: yarl<2.0,>=1.17.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.4) (1.18.3)
Requirement already satisfied: idna>=2.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from yarl<2.0,>=1.17.0->aiohttp==3.12.4) (3.10)
Using cached aiohttp-3.12.4-cp313-cp313-win_amd64.whl (439 kB)
Installing collected packages: aiohttp
  Attempting uninstall: aiohttp
    Found existing installation: aiohttp 3.12.3
    Uninstalling aiohttp-3.12.3:
      Successfully uninstalled aiohttp-3.12.3
Successfully installed aiohttp-3.12.4
 
C:\Users\topsp\OneDrive\Desktop\project>python script.py
Closing session...
Traceback (most recent call last):
  File "C:\Users\topsp\AppData\Local\Programs\Python\Python313\Lib\asyncio\runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "C:\Users\topsp\AppData\Local\Programs\Python\Python313\Lib\asyncio\base_events.py", line 725, in run_until_complete
    return future.result()
           ~~~~~~~~~~~~~^^
  File "C:\Users\topsp\OneDrive\Desktop\project\script.py", line 15, in main
    await connector.close()
  File "C:\Users\topsp\AppData\Local\Programs\Python\Python313\Lib\site-packages\aiohttp\connector.py", line 137, in _wait_for_close
    results = await asyncio.gather(*waiters, return_exceptions=True)
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
asyncio.exceptions.CancelledError
 
During handling of the above exception, another exception occurred:
 
Traceback (most recent call last):
  File "C:\Users\topsp\OneDrive\Desktop\project\script.py", line 21, in <module>
    asyncio.run(main(), debug=True)
    ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\topsp\AppData\Local\Programs\Python\Python313\Lib\asyncio\runners.py", line 195, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
  File "C:\Users\topsp\AppData\Local\Programs\Python\Python313\Lib\asyncio\runners.py", line 123, in run
    raise KeyboardInterrupt()
KeyboardInterrupt
```
**3.12.4**:
```
C:\Users\topsp\OneDrive\Desktop\project>pip install aiohttp==3.12.3
Collecting aiohttp==3.12.3
  Using cached aiohttp-3.12.3-cp313-cp313-win_amd64.whl.metadata (7.9 kB)
Requirement already satisfied: aiohappyeyeballs>=2.5.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.3) (2.6.1)
Requirement already satisfied: aiosignal>=1.1.2 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.3) (1.3.2)
Requirement already satisfied: attrs>=17.3.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.3) (25.3.0)
Requirement already satisfied: frozenlist>=1.1.1 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.3) (1.5.0)
Requirement already satisfied: multidict<7.0,>=4.5 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.3) (6.1.0)
Requirement already satisfied: propcache>=0.2.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.3) (0.2.1)
Requirement already satisfied: yarl<2.0,>=1.17.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from aiohttp==3.12.3) (1.18.3)
Requirement already satisfied: idna>=2.0 in c:\users\topsp\appdata\roaming\python\python313\site-packages (from yarl<2.0,>=1.17.0->aiohttp==3.12.3) (3.10)
Using cached aiohttp-3.12.3-cp313-cp313-win_amd64.whl (438 kB)
Installing collected packages: aiohttp
  Attempting uninstall: aiohttp
    Found existing installation: aiohttp 3.12.4
    Uninstalling aiohttp-3.12.4:
      Successfully uninstalled aiohttp-3.12.4
Successfully installed aiohttp-3.12.3
 
C:\Users\topsp\OneDrive\Desktop\project>python script.py
Closing session...
Done
```

### Python Version

```console
Python 3.13.5
```

### aiohttp Version

```console
aiohttp==3.12.13
```

### multidict Version

```console
multidict==6.6.3
```

### propcache Version

```console
propcache==0.3.2
```

### yarl Version

```console
yarl==1.20.1
```

### OS

Windows 10

### Related component

Client

### Additional context

Thank you for investigating this issue.

### Code of Conduct

- [x] I agree to follow the aio-libs Code of Conduct
date: 2025-07-09
patch: diff --git a/CHANGES/11273.bugfix.rst b/CHANGES/11273.bugfix.rst
new file mode 100644
index 000000000..b4d9948fb
--- /dev/null
+++ b/CHANGES/11273.bugfix.rst
@@ -0,0 +1,1 @@
+Fixed :py:meth:`ClientSession.close() <aiohttp.ClientSession.close>` hanging indefinitely when using HTTPS requests through HTTP proxies -- by :user:`bdraco`.
diff --git a/aiohttp/connector.py b/aiohttp/connector.py
index 1c96ff424..e1288424b 100644
--- a/aiohttp/connector.py
+++ b/aiohttp/connector.py
@@ -202,6 +202,26 @@ class Connection:
         return self._protocol is None or not self._protocol.is_connected()
 
 
+class _ConnectTunnelConnection(Connection):
+    """Special connection wrapper for CONNECT tunnels that must never be pooled.
+
+    This connection wraps the proxy connection that will be upgraded with TLS.
+    It must never be released to the pool because:
+    1. Its 'closed' future will never complete, causing session.close() to hang
+    2. It represents an intermediate state, not a reusable connection
+    3. The real connection (with TLS) will be created separately
+    """
+
+    def release(self) -> None:
+        """Do nothing - don't pool or close the connection.
+
+        These connections are an intermediate state during the CONNECT tunnel
+        setup and will be cleaned up naturally after the TLS upgrade. If they
+        were to be pooled, they would never be properly closed, causing
+        session.close() to wait forever for their 'closed' future.
+        """
+
+
 class _TransportPlaceholder:
     """placeholder for BaseConnector.connect function"""
 
@@ -1496,7 +1516,7 @@ class TCPConnector(BaseConnector):
             key = req.connection_key._replace(
                 proxy=None, proxy_auth=None, proxy_headers_hash=None
             )
-            conn = Connection(self, key, proto, self._loop)
+            conn = _ConnectTunnelConnection(self, key, proto, self._loop)
             proxy_resp = await proxy_req.send(conn)
             try:
                 protocol = conn._protocol

test_patch: diff --git a/tests/test_connector.py b/tests/test_connector.py
index 9932dee5..a01d31f3 100644
--- a/tests/test_connector.py
+++ b/tests/test_connector.py
@@ -4340,3 +4340,32 @@ async def test_connect_tunnel_connection_release(
 
     # Clean up to avoid resource warning
     conn.close()
+    _ConnectTunnelConnection,
+
+
+async def test_connect_tunnel_connection_release(
+    loop: asyncio.AbstractEventLoop,
+) -> None:
+    """Test _ConnectTunnelConnection.release() does not pool the connection."""
+    connector = mock.create_autospec(
+        aiohttp.BaseConnector, spec_set=True, instance=True
+    )
+    key = mock.create_autospec(ConnectionKey, spec_set=True, instance=True)
+    protocol = mock.create_autospec(ResponseHandler, spec_set=True, instance=True)
+
+    # Create a connect tunnel connection
+    conn = _ConnectTunnelConnection(connector, key, protocol, loop)
+
+    # Verify protocol is set
+    assert conn._protocol is protocol
+
+    # Release should do nothing (not pool the connection)
+    conn.release()
+
+    # Protocol should still be there (not released to pool)
+    assert conn._protocol is protocol
+    # Connector._release should NOT have been called
+    connector._release.assert_not_called()
+
+    # Clean up to avoid resource warning
+    conn.close()
\ No newline at end of file
diff --git a/tests/test_proxy_functional.py b/tests/test_proxy_functional.py
index f4bc020d..2ec40c8d 100644
--- a/tests/test_proxy_functional.py
+++ b/tests/test_proxy_functional.py
@@ -929,3 +929,47 @@ async def test_https_proxy_connect_tunnel_session_close_no_hang(
     finally:
         # Clean close
         await session.close()
+from contextlib import suppress
+
+
+async def test_https_proxy_connect_tunnel_session_close_no_hang(
+    aiohttp_server: AiohttpServer,
+) -> None:
+    """Test that CONNECT tunnel connections are not pooled."""
+    # Regression test for issue #11273.
+
+    # Create a minimal proxy server
+    # The CONNECT method is handled at the protocol level, not by the handler
+    proxy_app = web.Application()
+    proxy_server = await aiohttp_server(proxy_app)
+    proxy_url = f"http://{proxy_server.host}:{proxy_server.port}"
+
+    # Create session and make HTTPS request through proxy
+    session = aiohttp.ClientSession()
+
+    try:
+        # This will fail during TLS upgrade because proxy doesn't establish tunnel
+        with suppress(aiohttp.ClientError):
+            async with session.get("https://example.com/test", proxy=proxy_url) as resp:
+                await resp.read()
+
+        # The critical test: Check if any connections were pooled with proxy=None
+        # This is the root cause of the hang - CONNECT tunnel connections
+        # should NOT be pooled
+        connector = session.connector
+        assert connector is not None
+
+        # Count connections with proxy=None in the pool
+        proxy_none_keys = [key for key in connector._conns if key.proxy is None]
+        proxy_none_count = len(proxy_none_keys)
+
+        # Before the fix, there would be a connection with proxy=None
+        # After the fix, CONNECT tunnel connections are not pooled
+        assert proxy_none_count == 0, (
+            f"Found {proxy_none_count} connections with proxy=None in pool. "
+            f"CONNECT tunnel connections should not be pooled - this is bug #11273"
+        )
+
+    finally:
+        # Clean close
+        await session.close()
\ No newline at end of file