Skip to content

http.client.HTTPSConnection._tunnel raises AttributeError: 'NoneType' object has no attribute 'encode' when header value is None #159044

Closed

Description

@gaio-kimura

Bug report

Bug Description

Calling http.client.HTTPSConnection._tunnel() raises AttributeError: 'NoneType' object has no attribute 'encode' when _tunnel_headers contains a header key with a None value.

This regression affects third-party libraries (such as Apache Thrift and CodeChecker) that pass header dictionaries containing None values into set_tunnel(..., headers=...) or HTTP connection objects.

In Python 3.10.21, _tunnel() introduced strict encoding of tunnel headers (value.encode("latin-1")) without checking if the header value is None.

Target Python Version

Python 3.10.21 (Regression introduced between 3.10.20 and 3.10.21)

Operating System

Linux / Ubuntu

Code to Reproduce

import http.client

conn = http.client.HTTPSConnection("127.0.0.1", 8080)

# Set tunnel with a header value of None
conn.set_tunnel("example.com", 443, headers={"X-Test-Header": None})

# Call _tunnel() to trigger the issue
conn._tunnel()

Expected Behavior

_tunnel() should gracefully handle or filter out header keys with None values (or handle string conversions) rather than crashing with an AttributeError.

Actual Behavior

Traceback (most recent call last):
  File "reproduce.py", line 9, in <module>
    conn._tunnel()
  File "/usr/lib/python3.10/http/client.py", line 936, in _tunnel
    value_bytes = value.encode("latin-1")
AttributeError: 'NoneType' object has no attribute 'encode'

CPython versions tested on:

3.10

Operating systems tested on:

Linux

Linked PRs

Activity

  1. edvilme commented on Oct 10, 2026

    @edvilme
    Contributor

    I think we should convert to str either way

    As for the None value, I think that would require different considerations as to (1) omit, (2) error, (3) convert to "None", (4) convert to ""

    I would lean towards 3 if it matches old behavior the closest (my opinion, not a core dev), but perhaps you could open an issue on discuss.python.org to get more exposure and opinions:)

  2. bielpieroli commented on Oct 10, 2026

    @bielpieroli
    Contributor

    Agreed on converting to str in general, that restores the old behavior for ints and other objects.

    For None I'd lean towards (1) omit, but that's only the way I see it and, as I said, I'm new to the community.

    With (3) we keep sending Proxy-Authorization: None, and with (4) we'd still send an auth header, just with an empty value. Omitting it matches what the caller actually means: "there is no value for this header". I think that's how requests treats a header set to None.

    It would be simple: skip the header if the value is None, otherwise str(value) and then the CR/LF check as today.

    I've never opened a thread on discuss.python.org, but I'm happy to do it if you think that's needed. This is my first contribution here and I'm really glad to help :D

  3. LorenaMBorges commented on Oct 10, 2026

    @LorenaMBorges
    Contributor

    I also agree that it’s important to preserve the current behavior so we don’t introduce a breaking change for existing users of this API and to keep it stable. That’s why I like the idea of returning "None" as a string.

    That said, since Python is a language built for the future, it would be more versatile and more appropriate to follow industry standards and return null, that is, a proper None.

    The first option benefits people who already depend on this behavior, while the second benefits those who will adopt it later. Out of respect for current users, we could emit a deprecation warning announcing the change and then make it effective after a transition period. That way, everyone is happy.

  4. LorenaMBorges commented on Oct 10, 2026

    @LorenaMBorges
    Contributor

    Just to clarify, I'm working with @bielpieroli, and as suggested to him by @edvilme, I started a discussion topic. If you'd like to follow the discussion, it's here.

  5. ZeroIntensity commented on Oct 11, 2026

    @ZeroIntensity
    Member

    I think we should just str() both the key and the value, since that's what the previous behavior was. It should be as simple as:

    diff --git a/Lib/http/client.py b/Lib/http/client.py
    index 4b6fc549a9b..12525a7c897 100644
    --- a/Lib/http/client.py
    +++ b/Lib/http/client.py
    @@ -1014,8 +1014,8 @@ def _tunnel(self):
                 self._http_vsn_str.encode("ascii"))
             headers = [connect]
             for header, value in self._tunnel_headers.items():
    -            header_bytes = header.encode("latin-1")
    -            value_bytes = value.encode("latin-1")
    +            header_bytes = str(header).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):

    It'd also be nice if we explicitly documented that the headers are converted to a string.

  6. pfmoore commented on Oct 11, 2026

    @pfmoore
    Member

    This is a private API. Why are people even using it, and why are we considering providing support for such usage? If we want to make it supported usage, let's expose a new, official method (which can simply be called tunnel) and give it whatever semantics make the most sense for future users. Existing users can continue to use the private method (working around its limitations and quirks as needed) until they can transition to the new, supported one.

  7. edvilme commented on Oct 11, 2026

    @edvilme
    Contributor

    Calling .connect() with tunnels set internally calls the private method, so the bug is present in regular case scenarios and even with 3rd party libraries

  8. added 2 commits that reference this issue on Oct 12, 2026
  9. ZeroIntensity commented on Oct 12, 2026

    @ZeroIntensity
    Member

    I had Codex track down libraries that are affected and I came up with a script to reproduce it:

    Details
    # /// script
    # dependencies = [
    #     "thrift==0.23.0",
    #     "pyhive==0.7.0",
    #     "impyla==0.24.0",
    #     "impala-shell==4.6.0",
    #     "codechecker==6.29.1",
    # ]
    #
    # [[tool.uv.dependency-metadata]]
    # name = "impala-shell"
    # version = "4.6.0"
    # requires-dist = ["six==1.17.0", "thrift==0.23.0"]
    # ///
    
    import os
    import socket
    from contextlib import contextmanager
    
    
    @contextmanager
    def try_library(name):
        try:
            yield
        except AttributeError:
            print(name, "is affected!")
    
    
    with socket.create_server(("127.0.0.1", 0)) as proxy:
        os.environ["https_proxy"] = f"http://127.0.0.1:{proxy.getsockname()[1]}"
        os.environ["no_proxy"] = ""
    
        with try_library("thrift"):
            from thrift.transport.THttpClient import THttpClient
    
            THttpClient("https://example.invalid/api").flush()
    
        with try_library("pyhive"):
            from pyhive import hive
    
            hive.connect(host="example.invalid", port=443, scheme="https", username="test")
    
        with try_library("impyla"):
            from impala.dbapi import connect
    
            connect(
                host="example.invalid",
                port=443,
                use_http_transport=True,
                use_ssl=True,
                http_path="cliservice",
                timeout=1,
                retries=1,
            ).cursor()
    
        with try_library("impala_shell"):
            from impala_shell.ImpalaHttpClient import ImpalaHttpClient
    
            ImpalaHttpClient("https://example.invalid/api").open()
    
        with try_library("CodeChecker"):
            from codechecker_client.helpers.authentication import ThriftAuthHelper
    
            client = ThriftAuthHelper("https", "example.invalid", 443, "/v6/Authentication")
            client.getAuthParameters()

    So, concretely, (at least) all of the following are affected:

    1. Apache Thrift
    2. Impyla
    3. impala_shell
    4. CodeChecker
    5. PyHive

    But, I couldn't find any reports on their issue trackers, so I guess this has just gone unnoticed so far.

  10. added 3 commits that reference this issue on Oct 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    3.14bugs and security fixes3.15bugs and security fixes3.16new features, bugs and security fixesstdlibStandard Library Python modules in the Lib/ directorytype-bugAn unexpected behavior, bug, or error

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions