diff --git a/CHANGES/11713.bugfix.rst b/CHANGES/11713.bugfix.rst new file mode 100644 index 00000000000..dbb45a5254f --- /dev/null +++ b/CHANGES/11713.bugfix.rst @@ -0,0 +1 @@ +Fixed loading netrc credentials from the default :file:`~/.netrc` (:file:`~/_netrc` on Windows) location when the :envvar:`NETRC` environment variable is not set -- by :user:`bdraco`. diff --git a/CHANGES/11714.bugfix.rst b/CHANGES/11714.bugfix.rst new file mode 120000 index 00000000000..5a506f1ded3 --- /dev/null +++ b/CHANGES/11714.bugfix.rst @@ -0,0 +1 @@ +11713.bugfix.rst \ No newline at end of file diff --git a/aiohttp/client.py b/aiohttp/client.py index 7a4ad715362..d42b1469d43 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -590,14 +590,7 @@ async def _request( auth = self._default_auth # Try netrc if auth is still None and trust_env is enabled. - # Only check if NETRC environment variable is set to avoid - # creating an expensive executor job unnecessarily. - if ( - auth is None - and self._trust_env - and url.host is not None - and os.environ.get("NETRC") - ): + if auth is None and self._trust_env and url.host is not None: auth = await self._loop.run_in_executor( None, self._get_netrc_auth, url.host ) diff --git a/tests/conftest.py b/tests/conftest.py index 6833d2c1653..e7b1d417ddf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,7 @@ import asyncio import base64 import os +import platform import socket import ssl import sys @@ -316,6 +317,23 @@ def netrc_other_host(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path: return netrc_file +@pytest.fixture +def netrc_home_directory(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path: + """Create a netrc file in a mocked home directory without setting NETRC env var.""" + home_dir = tmp_path / "home" + home_dir.mkdir() + netrc_filename = "_netrc" if platform.system() == "Windows" else ".netrc" + netrc_file = home_dir / netrc_filename + netrc_file.write_text("default login netrc_user password netrc_pass\n") + + home_env_var = "USERPROFILE" if platform.system() == "Windows" else "HOME" + monkeypatch.setenv(home_env_var, str(home_dir)) + # Ensure NETRC env var is not set + monkeypatch.delenv("NETRC", raising=False) + + return netrc_file + + @pytest.fixture def start_connection() -> Iterator[mock.Mock]: with mock.patch( diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 731878d7c1b..9ccacb873b0 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -3775,12 +3775,12 @@ async def test_netrc_auth_from_env( # type: ignore[misc] @pytest.mark.usefixtures("no_netrc") -async def test_netrc_auth_skipped_without_env_var( # type: ignore[misc] +async def test_netrc_auth_skipped_without_netrc_file( # type: ignore[misc] headers_echo_client: Callable[ ..., Awaitable[TestClient[web.Request, web.Application]] ], ) -> None: - """Test that netrc authentication is skipped when NETRC env var is not set.""" + """Test that netrc authentication is skipped when no netrc file exists.""" client = await headers_echo_client(trust_env=True) async with client.get("/") as r: assert r.status == 200 @@ -3789,6 +3789,20 @@ async def test_netrc_auth_skipped_without_env_var( # type: ignore[misc] assert "Authorization" not in content["headers"] +@pytest.mark.usefixtures("netrc_home_directory") +async def test_netrc_auth_from_home_directory( # type: ignore[misc] + headers_echo_client: Callable[ + ..., Awaitable[TestClient[web.Request, web.Application]] + ], +) -> None: + """Test that netrc authentication works from default ~/.netrc without NETRC env var.""" + client = await headers_echo_client(trust_env=True) + async with client.get("/") as r: + assert r.status == 200 + content = await r.json() + assert content["headers"]["Authorization"] == "Basic bmV0cmNfdXNlcjpuZXRyY19wYXNz" + + @pytest.mark.usefixtures("netrc_default_contents") async def test_netrc_auth_overridden_by_explicit_auth( # type: ignore[misc] headers_echo_client: Callable[ diff --git a/tests/test_client_session.py b/tests/test_client_session.py index 84a417f9219..1248fb56fa4 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -1368,8 +1368,8 @@ async def test_netrc_auth_skipped_without_trust_env(auth_server: TestServer) -> @pytest.mark.usefixtures("no_netrc") -async def test_netrc_auth_skipped_without_netrc_env(auth_server: TestServer) -> None: - """Test that netrc authentication is skipped when NETRC env var is not set.""" +async def test_netrc_auth_skipped_without_netrc_file(auth_server: TestServer) -> None: + """Test that netrc authentication is skipped when no netrc file exists.""" async with ( ClientSession(trust_env=True) as session, session.get(auth_server.make_url("/")) as resp, @@ -1378,6 +1378,17 @@ async def test_netrc_auth_skipped_without_netrc_env(auth_server: TestServer) -> assert text == "no_auth" +@pytest.mark.usefixtures("netrc_home_directory") +async def test_netrc_auth_from_home_directory(auth_server: TestServer) -> None: + """Test that netrc authentication works from default ~/.netrc location without NETRC env var.""" + async with ( + ClientSession(trust_env=True) as session, + session.get(auth_server.make_url("/")) as resp, + ): + text = await resp.text() + assert text == "auth:Basic bmV0cmNfdXNlcjpuZXRyY19wYXNz" + + @pytest.mark.usefixtures("netrc_default_contents") async def test_netrc_auth_overridden_by_explicit_auth(auth_server: TestServer) -> None: """Test that explicit auth parameter overrides netrc authentication."""