Repository: aio-libs/aiohttp

Issue:
"Unclosed client session" when initialization fails
### Describe the bug

This is actually a wrong use case.

Some parameters will be checked after the connection is established.

So the connection cannot be closed if there are incorrect parameters.

```python
# -*- coding: utf-8 -*-
import asyncio

from aiohttp import ClientSession


async def main():
    async with ClientSession(timeout=1):
        pass


if __name__ == '__main__':
    asyncio.run(main())
```

How about deferring connection creation ( [here](https://github.com/aio-libs/aiohttp/blob/aa014a9c1084368a01a79f09bb238486e0aa164a/aiohttp/client.py#L257) ) ?

### To Reproduce

as above

### Expected behavior

First check that the parameters are correct and then establish the connection.

### Logs/tracebacks

```python-traceback
Traceback (most recent call last):
  File "D:\proj\test.py", line 13, in <module>
    asyncio.run(main())
  File "C:\Users\user\Miniconda3\Lib\asyncio\runners.py", line 190, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "C:\Users\user\Miniconda3\Lib\asyncio\runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\user\Miniconda3\Lib\asyncio\base_events.py", line 653, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "D:\proj\test.py", line 8, in main
    async with ClientSession(timeout=1):
               ^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\proj\.venv\Lib\site-packages\aiohttp\client.py", line 292, in __init__
    raise ValueError(
ValueError: timeout parameter cannot be of <class 'int'> type, please use 'timeout=ClientTimeout(...)'
Unclosed client session
client_session: <aiohttp.client.ClientSession object at 0x000001D74DC83CD0>
```


### Python Version

```console
$ python --version
Python 3.11.6
```


### aiohttp Version

```console
$ python -m pip show aiohttp
Name: aiohttp
Version: 3.9.3                                       
Summary: Async http client/server framework (asyncio)
Home-page: https://github.com/aio-libs/aiohttp       
Author:                                              
Author-email:
License: Apache 2
Location: D:\proj\.venv\Lib\site-packages
Requires: aiosignal, attrs, frozenlist, multidict, yarl
Required-by:
```


### multidict Version

```console
$ python -m pip show multidict
Name: multidict
Version: 6.0.5
Summary: multidict implementation
Home-page: https://github.com/aio-libs/multidict
Author: Andrew Svetlov
Author-email: andrew.svetlov@gmail.com
License: Apache 2
Location: D:\proj\.venv\Lib\site-packages
Requires:
Required-by: aiohttp, yarl
```


### yarl Version

```console
$ python -m pip show yarl
Name: yarl
Version: 1.9.4
Summary: Yet another URL library
Home-page: https://github.com/aio-libs/yarl
Author: Andrew Svetlov
Author-email: andrew.svetlov@gmail.com
License: Apache-2.0
Location: D:\proj\.venv\Lib\site-packages
Requires: idna, multidict
Required-by: aiohttp
```


### OS

Windows 10

### Related component

Client

### Additional context

_No response_

### Code of Conduct

- [X] I agree to follow the aio-libs Code of Conduct
"Unclosed client session" when initialization fails
### Describe the bug

This is actually a wrong use case.

Some parameters will be checked after the connection is established.

So the connection cannot be closed if there are incorrect parameters.

```python
# -*- coding: utf-8 -*-
import asyncio

from aiohttp import ClientSession


async def main():
    async with ClientSession(timeout=1):
        pass


if __name__ == '__main__':
    asyncio.run(main())
```

How about deferring connection creation ( [here](https://github.com/aio-libs/aiohttp/blob/aa014a9c1084368a01a79f09bb238486e0aa164a/aiohttp/client.py#L257) ) ?

### To Reproduce

as above

### Expected behavior

First check that the parameters are correct and then establish the connection.

### Logs/tracebacks

```python-traceback
Traceback (most recent call last):
  File "D:\proj\test.py", line 13, in <module>
    asyncio.run(main())
  File "C:\Users\user\Miniconda3\Lib\asyncio\runners.py", line 190, in run
    return runner.run(main)
           ^^^^^^^^^^^^^^^^
  File "C:\Users\user\Miniconda3\Lib\asyncio\runners.py", line 118, in run
    return self._loop.run_until_complete(task)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\user\Miniconda3\Lib\asyncio\base_events.py", line 653, in run_until_complete
    return future.result()
           ^^^^^^^^^^^^^^^
  File "D:\proj\test.py", line 8, in main
    async with ClientSession(timeout=1):
               ^^^^^^^^^^^^^^^^^^^^^^^^
  File "D:\proj\.venv\Lib\site-packages\aiohttp\client.py", line 292, in __init__
    raise ValueError(
ValueError: timeout parameter cannot be of <class 'int'> type, please use 'timeout=ClientTimeout(...)'
Unclosed client session
client_session: <aiohttp.client.ClientSession object at 0x000001D74DC83CD0>
```


### Python Version

```console
$ python --version
Python 3.11.6
```


### aiohttp Version

```console
$ python -m pip show aiohttp
Name: aiohttp
Version: 3.9.3                                       
Summary: Async http client/server framework (asyncio)
Home-page: https://github.com/aio-libs/aiohttp       
Author:                                              
Author-email:
License: Apache 2
Location: D:\proj\.venv\Lib\site-packages
Requires: aiosignal, attrs, frozenlist, multidict, yarl
Required-by:
```


### multidict Version

```console
$ python -m pip show multidict
Name: multidict
Version: 6.0.5
Summary: multidict implementation
Home-page: https://github.com/aio-libs/multidict
Author: Andrew Svetlov
Author-email: andrew.svetlov@gmail.com
License: Apache 2
Location: D:\proj\.venv\Lib\site-packages
Requires:
Required-by: aiohttp, yarl
```


### yarl Version

```console
$ python -m pip show yarl
Name: yarl
Version: 1.9.4
Summary: Yet another URL library
Home-page: https://github.com/aio-libs/yarl
Author: Andrew Svetlov
Author-email: andrew.svetlov@gmail.com
License: Apache-2.0
Location: D:\proj\.venv\Lib\site-packages
Requires: idna, multidict
Required-by: aiohttp
```


### OS

Windows 10

### Related component

Client

### Additional context

_No response_

### Code of Conduct

- [X] I agree to follow the aio-libs Code of Conduct


Discussion:
Feel free to make a PR and move the check earlier.
Feel free to make a PR and move the check earlier.

Resolving patch:
diff --git a/aiohttp/client.py b/aiohttp/client.py
--- a/aiohttp/client.py
+++ b/aiohttp/client.py
@@ -243,6 +243,9 @@ def __init__(
         max_field_size: int = 8190,
         fallback_charset_resolver: _CharsetResolver = lambda r, b: "utf-8",
     ) -> None:
+        # We initialise _connector to None immediately, as it's referenced in __del__()
+        # and could cause issues if an exception occurs during initialisation.
+        self._connector: Optional[BaseConnector] = None
         if base_url is None or isinstance(base_url, URL):
             self._base_url: Optional[URL] = base_url
         else:
@@ -253,12 +256,20 @@ def __init__(
 
         loop = asyncio.get_running_loop()
 
+        if timeout is sentinel or timeout is None:
+            timeout = DEFAULT_TIMEOUT
+        if not isinstance(timeout, ClientTimeout):
+            raise ValueError(
+                f"timeout parameter cannot be of {type(timeout)} type, "
+                "please use 'timeout=ClientTimeout(...)'",
+            )
+        self._timeout = timeout
+
         if connector is None:
             connector = TCPConnector()
-
         # Initialize these three attrs before raising any exception,
         # they are used in __del__
-        self._connector: Optional[BaseConnector] = connector
+        self._connector = connector
         self._loop = loop
         if loop.get_debug():
             self._source_traceback: Optional[traceback.StackSummary] = (
@@ -281,14 +292,6 @@ def __init__(
         self._default_auth = auth
         self._version = version
         self._json_serialize = json_serialize
-        if timeout is sentinel or timeout is None:
-            timeout = DEFAULT_TIMEOUT
-        if not isinstance(timeout, ClientTimeout):
-            raise ValueError(
-                f"timeout parameter cannot be of {type(timeout)} type, "
-                "please use 'timeout=ClientTimeout(...)'",
-            )
-        self._timeout = timeout
         self._raise_for_status = raise_for_status
         self._auto_decompress = auto_decompress
         self._trust_env = trust_env


Test patch:
diff --git a/tests/test_client_session.py b/tests/test_client_session.py
--- a/tests/test_client_session.py
+++ b/tests/test_client_session.py
@@ -841,3 +841,8 @@ async def test_build_url_returns_expected_url(
 ) -> None:
     session = await create_session(base_url)
     assert session._build_url(url) == expected_url
+
+
+async def test_instantiation_with_invalid_timeout_value():
+    with pytest.raises(ValueError, match="timeout parameter cannot be .*"):
+        ClientSession(timeout=1)