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 b99f834d0bc..8d2c3d67921 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -645,14 +645,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 bde9500f129..1a7be393358 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 @@ -309,6 +310,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 7ff53719146..4cf18b9e5ed 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -3777,12 +3777,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 @@ -3791,6 +3791,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 ade8a67b7ca..7ab98c2bee4 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -1366,8 +1366,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, @@ -1376,6 +1376,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."""