diff --git a/py/services/downloader.py b/py/services/downloader.py index 3925d093..2697ba26 100644 --- a/py/services/downloader.py +++ b/py/services/downloader.py @@ -156,6 +156,25 @@ class DownloadStalledError(Exception): """Raised when download progress stalls beyond the configured timeout.""" +def _disable_netrc_auth(session: aiohttp.ClientSession) -> None: + """Prevent the session from loading credentials from netrc files. + + ``trust_env=True`` is kept so system-level proxies still work, but aiohttp + would also auto-apply netrc entries (e.g. ``machine civitai.red``) as + BasicAuth. aiohttp refuses to combine those with the explicit + ``Authorization: Bearer`` header set for CivitAI requests, raising + "Cannot combine AUTHORIZATION header with AUTH argument or credentials + encoded in URL" before the request is even sent. Subclassing ClientSession + is discouraged by aiohttp (emits a DeprecationWarning), so the private + hook is patched on the instance instead. + """ + + def _no_netrc_auth(*args: Any, **kwargs: Any) -> Optional[aiohttp.BasicAuth]: + return None + + setattr(session, "_get_netrc_auth", _no_netrc_auth) + + class Downloader: """Unified downloader for all HTTP/HTTPS downloads in the application.""" @@ -370,6 +389,7 @@ class Downloader: trust_env=not app_proxy_active, timeout=timeout, ) + _disable_netrc_auth(self._session) # Store proxy URL for per-request use. Stays None for SOCKS because the # ProxyConnector already tunnels everything; passing proxy= for SOCKS diff --git a/tests/services/test_downloader.py b/tests/services/test_downloader.py index ab276857..94ee8e74 100644 --- a/tests/services/test_downloader.py +++ b/tests/services/test_downloader.py @@ -272,3 +272,28 @@ async def test_download_file_retries_redirected_url_when_range_not_honored(tmp_p assert _session(downloader).requests[0]["headers"]["Range"] == "bytes=3-" assert _session(downloader).requests[1]["url"] == redirected_url assert _session(downloader).requests[1]["headers"]["Range"] == "bytes=3-" + + +@pytest.mark.asyncio +async def test_disable_netrc_auth_ignores_netrc_file(tmp_path, monkeypatch): + """netrc entries must not be auto-applied as BasicAuth. + + Regression test for "Cannot combine AUTHORIZATION header with AUTH + argument or credentials encoded in URL": with trust_env=True aiohttp + loads credentials from netrc files, which conflict with the explicit + Authorization: Bearer header used for CivitAI requests. + """ + import aiohttp + + from py.services.downloader import _disable_netrc_auth + + netrc_file = tmp_path / ".netrc" + netrc_file.write_text("machine civitai.red\nlogin user\npassword pass\n") + monkeypatch.setenv("NETRC", str(netrc_file)) + + async with aiohttp.ClientSession(trust_env=True) as session: + # Premise: a plain session would pick up the netrc credentials. + assert session._get_netrc_auth("civitai.red") is not None + + _disable_netrc_auth(session) + assert session._get_netrc_auth("civitai.red") is None