From d6b22b66ee382d47dcf1cf35451c60c9f3b808ca Mon Sep 17 00:00:00 2001 From: Gregory Oschwald Date: Thu, 4 Jun 2026 15:58:26 +0000 Subject: [PATCH] Use aiohttp.encode_basic_auth() instead of deprecated BasicAuth aiohttp 3.14.0 deprecates aiohttp.BasicAuth and the auth= parameter on ClientSession, both slated for removal in aiohttp 4.0. Build the Authorization header explicitly with aiohttp.encode_basic_auth() in the async client instead, and raise the minimum aiohttp version to 3.14.0 (where encode_basic_auth is available). encode_basic_auth() defaults to utf-8 where BasicAuth defaulted to latin1. This is irrelevant here since account IDs are numeric and license keys are ASCII, but is worth noting. Adds a webservice test asserting the Authorization header is sent, for both the sync and async clients. STF-604 Co-Authored-By: Claude Opus 4.8 (1M context) --- HISTORY.rst | 4 ++++ pyproject.toml | 2 +- src/minfraud/webservice.py | 10 ++++++++-- tests/test_webservice.py | 11 +++++++++++ uv.lock | 2 +- 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/HISTORY.rst b/HISTORY.rst index ffa55e5..99fd48b 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -17,6 +17,10 @@ History * The version is now retrieved from package metadata at runtime using ``importlib.metadata``. This reduces the chance of version inconsistencies during releases. +* The async client now builds its ``Authorization`` header with + ``aiohttp.encode_basic_auth()`` instead of the ``aiohttp.BasicAuth`` / + ``auth=`` parameter, which are deprecated as of aiohttp 3.14.0. As a result, + the minimum required ``aiohttp`` version is now 3.14.0. 3.2.0 (2025-11-20) ++++++++++++++++++ diff --git a/pyproject.toml b/pyproject.toml index 5f2f633..b22c8f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = [ {name = "Gregory Oschwald", email = "goschwald@maxmind.com"}, ] dependencies = [ - "aiohttp>=3.6.2,<4.0.0", + "aiohttp>=3.14.0,<4.0.0", "email_validator>=2.0.0,<3.0.0", "geoip2>=5.2.0,<6.0.0", "requests>=2.24.0,<3.0.0", diff --git a/src/minfraud/webservice.py b/src/minfraud/webservice.py index 77fb2d3..69eb601 100644 --- a/src/minfraud/webservice.py +++ b/src/minfraud/webservice.py @@ -442,8 +442,14 @@ async def _do_request( async def _session(self) -> aiohttp.ClientSession: if not hasattr(self, "_existing_session"): self._existing_session = aiohttp.ClientSession( - auth=aiohttp.BasicAuth(self._account_id, self._license_key), - headers={"Accept": "application/json", "User-Agent": _AIOHTTP_UA}, + headers={ + "Accept": "application/json", + "Authorization": aiohttp.encode_basic_auth( + self._account_id, + self._license_key, + ), + "User-Agent": _AIOHTTP_UA, + }, timeout=aiohttp.ClientTimeout(total=self._timeout), ) diff --git a/tests/test_webservice.py b/tests/test_webservice.py index 919c7ac..2dfff37 100644 --- a/tests/test_webservice.py +++ b/tests/test_webservice.py @@ -237,6 +237,17 @@ def test_200(self) -> None: self.assertEqual("004", model.ip_address.traits.mobile_network_code) self.assertEqual("ANONYMOUS_IP", model.ip_address.risk_reasons[0].code) + def test_authorization_header(self) -> None: + # Credentials must be sent via the Authorization header rather than the + # deprecated aiohttp BasicAuth / auth= parameter. The expected value is + # base64("42:abcdef123456"). + self.create_success() + request, _ = self.httpserver.log[-1] + self.assertEqual( + "Basic NDI6YWJjZGVmMTIzNDU2", + request.headers.get("Authorization"), + ) + def test_200_on_request_with_nones(self) -> None: model = self.create_success( request={ diff --git a/uv.lock b/uv.lock index 50a6444..524fe20 100644 --- a/uv.lock +++ b/uv.lock @@ -742,7 +742,7 @@ lint = [ [package.metadata] requires-dist = [ - { name = "aiohttp", specifier = ">=3.6.2,<4.0.0" }, + { name = "aiohttp", specifier = ">=3.14.0,<4.0.0" }, { name = "email-validator", specifier = ">=2.0.0,<3.0.0" }, { name = "geoip2", specifier = ">=5.2.0,<6.0.0" }, { name = "requests", specifier = ">=2.24.0,<3.0.0" },