https://github.com/python/cpython/commit/8138bf11a5d32e8db003a86606be8e4c61e14d4b
commit: 8138bf11a5d32e8db003a86606be8e4c61e14d4b
branch: main
author: João Gabriel Pieroli da Silva <[email protected]>
committer: ZeroIntensity <[email protected]>
date: 2026-10-13T19:24:45-04:00
summary:

gh-159044: Restore string conversion for HTTP tunnel header values (GH-159216)

Co-authored-by: LorenaMBorges <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-10-12-00-00-00.gh-issue-159044.Tr8kXv.rst
M Doc/library/http.client.rst
M Lib/http/client.py
M Lib/test/test_httplib.py

diff --git a/Doc/library/http.client.rst b/Doc/library/http.client.rst
index 4002fc697ceaa56..ab557f1805e1049 100644
--- a/Doc/library/http.client.rst
+++ b/Doc/library/http.client.rst
@@ -370,7 +370,8 @@ HTTPConnection Objects
    proxy server).
 
    The *headers* argument should be a mapping of extra HTTP headers to send 
with
-   the CONNECT request.
+   the CONNECT request. Non-string header values are converted to strings via
+   :func:`str`.
 
    As HTTP/1.1 is used for HTTP CONNECT tunnelling request, `as per the RFC
    <https://datatracker.ietf.org/doc/html/rfc7231#section-4.3.6>`_, a HTTP 
``Host:``
diff --git a/Lib/http/client.py b/Lib/http/client.py
index 4b6fc549a9b5179..f13eb6d91b77f6f 100644
--- a/Lib/http/client.py
+++ b/Lib/http/client.py
@@ -1015,7 +1015,7 @@ def _tunnel(self):
         headers = [connect]
         for header, value in self._tunnel_headers.items():
             header_bytes = header.encode("latin-1")
-            value_bytes = value.encode("latin-1")
+            value_bytes = str(value).encode("latin-1")
             if not _is_legal_header_name(header_bytes):
                 raise ValueError('Invalid header name %r' % (header_bytes,))
             if _is_illegal_header_value(value_bytes):
diff --git a/Lib/test/test_httplib.py b/Lib/test/test_httplib.py
index ad28e921978e98f..5fd4e8fca30fcf5 100644
--- a/Lib/test/test_httplib.py
+++ b/Lib/test/test_httplib.py
@@ -2793,6 +2793,23 @@ def _create_connection(address, timeout=None, 
source_address=None):
         self.assertIsNotNone(exc)
         self.assertTrue(sock.file_closed)
 
+    def test_tunnel_nonstr_headers(self):
+        class CustomObject:
+            def __str__(self):
+                return 'custom-str'
+
+        self.conn.set_tunnel('destination.com', headers={
+            'None-Value': None,
+            'Integer-Value': 9,
+            'Custom-Value': CustomObject(),
+        })
+
+        self.conn.request('HEAD', '/', '')
+
+        self.assertIn(b'None-Value: None\r\n', self.conn.sock.data)
+        self.assertIn(b'Integer-Value: 9\r\n', self.conn.sock.data)
+        self.assertIn(b'Custom-Value: custom-str\r\n', self.conn.sock.data)
+
 
 if __name__ == '__main__':
     unittest.main(verbosity=2)
diff --git 
a/Misc/NEWS.d/next/Library/2026-10-12-00-00-00.gh-issue-159044.Tr8kXv.rst 
b/Misc/NEWS.d/next/Library/2026-10-12-00-00-00.gh-issue-159044.Tr8kXv.rst
new file mode 100644
index 000000000000000..ef51dc8eca1066c
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-10-12-00-00-00.gh-issue-159044.Tr8kXv.rst
@@ -0,0 +1,4 @@
+Restore the previous behaviour of :meth:`http.client.HTTPConnection.set_tunnel`
+where header values that are not strings (such as :const:`None`, :class:`int`
+or other objects) are converted with :class:`str` instead of raising
+:exc:`AttributeError` when the request is sent.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to