From d112833142e31c1d1ca5db12af0e5ed5a9370d8e Mon Sep 17 00:00:00 2001 From: wzr-bot Date: Mon, 23 Mar 2026 03:14:30 +0800 Subject: [PATCH 1/2] Fix SSL certificate verification failure for hostnames with trailing dots Python's ssl module does not handle trailing dots in server_hostname, causing CERTIFICATE_VERIFY_FAILED errors for fully qualified domain names (FQDNs) like 'example.com.'. This fix strips the trailing dot from server_hostname at the connection level before passing it to SSL backends, following the same approach used by urllib3. The underlying DNS resolution still uses the original hostname with the trailing dot. Fixes #1063 --- httpcore/_async/connection.py | 5 +++-- httpcore/_async/http_proxy.py | 2 +- httpcore/_async/socks_proxy.py | 5 +++-- httpcore/_sync/connection.py | 5 +++-- httpcore/_sync/http_proxy.py | 2 +- httpcore/_sync/socks_proxy.py | 5 +++-- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/httpcore/_async/connection.py b/httpcore/_async/connection.py index b42581dff..878d78523 100644 --- a/httpcore/_async/connection.py +++ b/httpcore/_async/connection.py @@ -148,8 +148,9 @@ async def _connect(self, request: Request) -> AsyncNetworkStream: kwargs = { "ssl_context": ssl_context, - "server_hostname": sni_hostname - or self._origin.host.decode("ascii"), + "server_hostname": ( + sni_hostname or self._origin.host.decode("ascii") + ).rstrip("."), "timeout": timeout, } async with Trace("start_tls", logger, request, kwargs) as trace: diff --git a/httpcore/_async/http_proxy.py b/httpcore/_async/http_proxy.py index cc9d92066..6e2c2e9e1 100644 --- a/httpcore/_async/http_proxy.py +++ b/httpcore/_async/http_proxy.py @@ -309,7 +309,7 @@ async def handle_async_request(self, request: Request) -> Response: kwargs = { "ssl_context": ssl_context, - "server_hostname": self._remote_origin.host.decode("ascii"), + "server_hostname": self._remote_origin.host.decode("ascii").rstrip("."), "timeout": timeout, } async with Trace("start_tls", logger, request, kwargs) as trace: diff --git a/httpcore/_async/socks_proxy.py b/httpcore/_async/socks_proxy.py index b363f55a0..489dae1f0 100644 --- a/httpcore/_async/socks_proxy.py +++ b/httpcore/_async/socks_proxy.py @@ -258,8 +258,9 @@ async def handle_async_request(self, request: Request) -> Response: kwargs = { "ssl_context": ssl_context, - "server_hostname": sni_hostname - or self._remote_origin.host.decode("ascii"), + "server_hostname": ( + sni_hostname or self._remote_origin.host.decode("ascii") + ).rstrip("."), "timeout": timeout, } async with Trace("start_tls", logger, request, kwargs) as trace: diff --git a/httpcore/_sync/connection.py b/httpcore/_sync/connection.py index 363f8be81..a5ac7a837 100644 --- a/httpcore/_sync/connection.py +++ b/httpcore/_sync/connection.py @@ -148,8 +148,9 @@ def _connect(self, request: Request) -> NetworkStream: kwargs = { "ssl_context": ssl_context, - "server_hostname": sni_hostname - or self._origin.host.decode("ascii"), + "server_hostname": ( + sni_hostname or self._origin.host.decode("ascii") + ).rstrip("."), "timeout": timeout, } with Trace("start_tls", logger, request, kwargs) as trace: diff --git a/httpcore/_sync/http_proxy.py b/httpcore/_sync/http_proxy.py index ecca88f7d..924f4c585 100644 --- a/httpcore/_sync/http_proxy.py +++ b/httpcore/_sync/http_proxy.py @@ -309,7 +309,7 @@ def handle_request(self, request: Request) -> Response: kwargs = { "ssl_context": ssl_context, - "server_hostname": self._remote_origin.host.decode("ascii"), + "server_hostname": self._remote_origin.host.decode("ascii").rstrip("."), "timeout": timeout, } with Trace("start_tls", logger, request, kwargs) as trace: diff --git a/httpcore/_sync/socks_proxy.py b/httpcore/_sync/socks_proxy.py index 0ca96ddfb..cfd52a0fe 100644 --- a/httpcore/_sync/socks_proxy.py +++ b/httpcore/_sync/socks_proxy.py @@ -258,8 +258,9 @@ def handle_request(self, request: Request) -> Response: kwargs = { "ssl_context": ssl_context, - "server_hostname": sni_hostname - or self._remote_origin.host.decode("ascii"), + "server_hostname": ( + sni_hostname or self._remote_origin.host.decode("ascii") + ).rstrip("."), "timeout": timeout, } with Trace("start_tls", logger, request, kwargs) as trace: From 1cf2cf76aecec9aef3839f8e44f307fb77811756 Mon Sep 17 00:00:00 2001 From: wzr-bot Date: Wed, 10 Jun 2026 15:57:50 +0800 Subject: [PATCH 2/2] Apply ruff format --- httpcore/_async/http_proxy.py | 4 +++- httpcore/_sync/http_proxy.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/httpcore/_async/http_proxy.py b/httpcore/_async/http_proxy.py index 6e2c2e9e1..9a766a642 100644 --- a/httpcore/_async/http_proxy.py +++ b/httpcore/_async/http_proxy.py @@ -309,7 +309,9 @@ async def handle_async_request(self, request: Request) -> Response: kwargs = { "ssl_context": ssl_context, - "server_hostname": self._remote_origin.host.decode("ascii").rstrip("."), + "server_hostname": self._remote_origin.host.decode("ascii").rstrip( + "." + ), "timeout": timeout, } async with Trace("start_tls", logger, request, kwargs) as trace: diff --git a/httpcore/_sync/http_proxy.py b/httpcore/_sync/http_proxy.py index 924f4c585..afe1efecd 100644 --- a/httpcore/_sync/http_proxy.py +++ b/httpcore/_sync/http_proxy.py @@ -309,7 +309,9 @@ def handle_request(self, request: Request) -> Response: kwargs = { "ssl_context": ssl_context, - "server_hostname": self._remote_origin.host.decode("ascii").rstrip("."), + "server_hostname": self._remote_origin.host.decode("ascii").rstrip( + "." + ), "timeout": timeout, } with Trace("start_tls", logger, request, kwargs) as trace: