From cdaeb387c7d6195ed06a810f97d4a39fc4f94313 Mon Sep 17 00:00:00 2001 From: Pierre-Louis Peeters Date: Fri, 28 Mar 2025 15:56:08 +0100 Subject: [PATCH] Add specific exceptions for authentication issues --- aiohttp/__init__.py | 6 ++++++ aiohttp/client.py | 20 +++++++++++++++++++- aiohttp/client_exceptions.py | 15 +++++++++++++++ aiohttp/client_reqrep.py | 14 +++++++++++++- tests/test_client_functional.py | 12 ++++++++++++ tests/test_client_request.py | 5 +++++ 6 files changed, 70 insertions(+), 2 deletions(-) diff --git a/aiohttp/__init__.py b/aiohttp/__init__.py index 7759a997cb9..3d5c2d6ea7f 100644 --- a/aiohttp/__init__.py +++ b/aiohttp/__init__.py @@ -27,7 +27,10 @@ ConnectionTimeoutError, ContentTypeError, Fingerprint, + InvalidAuthClientError, + InvalidRedirectUrlAuthClientError, InvalidURL, + InvalidUrlAuthClientError, InvalidUrlClientError, InvalidUrlRedirectClientError, NamedPipeConnector, @@ -137,7 +140,10 @@ "ConnectionTimeoutError", "ContentTypeError", "Fingerprint", + "InvalidAuthClientError", + "InvalidRedirectUrlAuthClientError", "InvalidURL", + "InvalidUrlAuthClientError", "InvalidUrlClientError", "InvalidUrlRedirectClientError", "NonHttpUrlClientError", diff --git a/aiohttp/client.py b/aiohttp/client.py index 04f03b710f0..3ea7caf0158 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -57,7 +57,10 @@ ClientSSLError, ConnectionTimeoutError, ContentTypeError, + InvalidAuthClientError, + InvalidRedirectUrlAuthClientError, InvalidURL, + InvalidUrlAuthClientError, InvalidUrlClientError, InvalidUrlRedirectClientError, NonHttpUrlClientError, @@ -124,7 +127,10 @@ "ClientSSLError", "ConnectionTimeoutError", "ContentTypeError", + "InvalidAuthClientError", + "InvalidRedirectUrlAuthClientError", "InvalidURL", + "InvalidUrlAuthClientError", "InvalidUrlClientError", "RedirectClientError", "NonHttpUrlClientError", @@ -576,7 +582,19 @@ async def _request( # Override the auth with the one from the URL only if we # have no auth, or if we got an auth from a redirect URL - if auth is None or (history and auth_from_url is not None): + if (auth is None or history) and auth_from_url is not None: + # Pre-check the credentials can be encoded to + # avoid crashing down the line + try: + auth_from_url.encode() + except UnicodeEncodeError as e: + auth_err_exc_cls = ( + InvalidRedirectUrlAuthClientError + if redirects + else InvalidUrlAuthClientError + ) + raise auth_err_exc_cls(url, str(e)) from e + auth = auth_from_url if ( diff --git a/aiohttp/client_exceptions.py b/aiohttp/client_exceptions.py index da159d0ae7d..94e4c18548f 100644 --- a/aiohttp/client_exceptions.py +++ b/aiohttp/client_exceptions.py @@ -48,9 +48,12 @@ "ContentTypeError", "ClientPayloadError", "InvalidURL", + "InvalidAuthClientError", + "InvalidUrlAuthClientError", "InvalidUrlClientError", "RedirectClientError", "NonHttpUrlClientError", + "InvalidRedirectUrlAuthClientError", "InvalidUrlRedirectClientError", "NonHttpUrlRedirectClientError", "WSMessageTypeError", @@ -306,6 +309,14 @@ class InvalidUrlClientError(InvalidURL): """Invalid URL client error.""" +class InvalidAuthClientError(ClientError): + """Invalid auth client error.""" + + +class InvalidUrlAuthClientError(InvalidURL): + """Invalid URL auth client error.""" + + class RedirectClientError(ClientError): """Client redirect error.""" @@ -318,6 +329,10 @@ class InvalidUrlRedirectClientError(InvalidUrlClientError, RedirectClientError): """Invalid URL redirect client error.""" +class InvalidRedirectUrlAuthClientError(InvalidUrlRedirectClientError): + """Invalid redirect URL auth client error.""" + + class NonHttpUrlRedirectClientError(NonHttpUrlClientError, RedirectClientError): """Non http URL redirect client error.""" diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index d30e8704d3e..0a9d998878f 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -35,7 +35,9 @@ ClientOSError, ClientResponseError, ContentTypeError, + InvalidAuthClientError, InvalidURL, + InvalidUrlAuthClientError, ServerFingerprintMismatch, ) from .compression_utils import HAS_BROTLI @@ -497,7 +499,11 @@ def update_transfer_encoding(self) -> None: def update_auth(self, auth: Optional[BasicAuth], trust_env: bool = False) -> None: """Set basic auth.""" if auth is None: + auth_from_url = True auth = self.auth + else: + auth_from_url = False + if auth is None and trust_env and self.url.host is not None: netrc_obj = netrc_from_env() with contextlib.suppress(LookupError): @@ -508,7 +514,13 @@ def update_auth(self, auth: Optional[BasicAuth], trust_env: bool = False) -> Non if not isinstance(auth, helpers.BasicAuth): raise TypeError("BasicAuth() tuple is required instead") - self.headers[hdrs.AUTHORIZATION] = auth.encode() + try: + self.headers[hdrs.AUTHORIZATION] = auth.encode() + except UnicodeEncodeError as e: + auth_err_exc_cls = ( + InvalidUrlAuthClientError if auth_from_url else InvalidAuthClientError + ) + raise auth_err_exc_cls(self.url, str(e)) from e def update_body_from_data(self, body: Any) -> None: if body is None: diff --git a/tests/test_client_functional.py b/tests/test_client_functional.py index 04538966062..b135d4738b2 100644 --- a/tests/test_client_functional.py +++ b/tests/test_client_functional.py @@ -38,6 +38,7 @@ from aiohttp.client_exceptions import ( ClientResponseError, InvalidURL, + InvalidUrlAuthClientError, InvalidUrlClientError, InvalidUrlRedirectClientError, NonHttpUrlClientError, @@ -2685,6 +2686,13 @@ async def handler_redirect(request: web.Request) -> web.Response: ("http:///example.com", "http:///example.com"), ) +INVALID_URL_WITH_ERROR_MESSAGE_BAD_AUTH = ( + ( + "http://badchar username@example.com", + "http://example.com - 'latin-?1' codec can't encode", + ), +) + NON_HTTP_URL_WITH_ERROR_MESSAGE = ( ("call:+380123456789", r"call:\+380123456789"), ("skype:handle", "skype:handle"), @@ -2706,6 +2714,10 @@ async def handler_redirect(request: web.Request) -> web.Response: (url, message, InvalidUrlClientError) for (url, message) in INVALID_URL_WITH_ERROR_MESSAGE_YARL_ORIGIN ), + *( + (url, message, InvalidUrlAuthClientError) + for (url, message) in INVALID_URL_WITH_ERROR_MESSAGE_BAD_AUTH + ), *( (url, message, NonHttpUrlClientError) for (url, message) in NON_HTTP_URL_WITH_ERROR_MESSAGE diff --git a/tests/test_client_request.py b/tests/test_client_request.py index 2b5e2725c49..ceed5b618de 100644 --- a/tests/test_client_request.py +++ b/tests/test_client_request.py @@ -378,6 +378,11 @@ def test_invalid_url(make_request: _RequestMaker) -> None: make_request("get", "hiwpefhipowhefopw") +def test_invalid_auth(make_request: _RequestMaker) -> None: + with pytest.raises(aiohttp.InvalidUrlAuthClientError): + make_request("get", "http://badchar username@example.com") + + def test_no_path(make_request: _RequestMaker) -> None: req = make_request("get", "http://python.org") assert "/" == req.url.path