You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
http.client.HTTPSConnection._tunnel raises AttributeError: 'NoneType' object has no attribute 'encode' when header value is None #159044
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
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'
Yes, Python 3.10 went EOL last week, so unfortunately, we can't change anything there. This does affect recent versions, though, since the regressing change (#146212) was backported to other versions, so we could potentially fix it in those.
That said, _tunnel is a private method, so you opt in to these sorts of breakages by using it. Do you have any data on how widespread the breakage is here (not just libraries that pass None to headers, but libraries that call _tunnel)? Or, ideally, a repro that doesn't use private APIs?
Is the desired behavior to not raise any exception? I think passing None qualifies as an "ilegal" header/value name and thus must raise the same error:
This issue breaks real-world CLI tools and applications such as Apache Thrift (THttpClient).
In practice, this commonly happens when configuring an HTTP proxy without user credentials via environment variables:
export http_proxy="http://YOUR_PROXY_HOST:PORT"
Libraries like Apache Thrift (THttpClient.flush()) construct request/tunnel headers dynamically.
When proxy authentication (username/password) or session credentials are not provided, these libraries set the corresponding header values to None (e.g. {"Proxy-Authorization": None} or unset auth token headers) and pass the dictionary to set_tunnel(..., headers=...).
Standard high-level operations (e.g., logging in, running analysis commands) trigger conn.connect(), which internally calls _tunnel() and crashes with: AttributeError: 'NoneType' object has no attribute 'encode'
Users encounter this failure simply by running standard CLI commands on unauthenticated proxy networks after upgrading Python.
3. Previous Behavior vs. Desired Behavior
Regarding whether None should raise a ValueError:
Previous Behavior (Python 3.10.20 and earlier):
Passing None as a header value did not raise an exception. Requests and handshakes through unauthenticated proxies worked seamlessly across numerous releases.
Current Behavior (Python 3.10.21):
The handshake immediately fails with AttributeError: 'NoneType' object has no attribute 'encode'.
Desired Behavior:
If Python strictly treats None as an illegal value and raises ValueError: Invalid header name/value None, it will still break existing real-world libraries and proxy setups that previously worked without issue.
For backwards compatibility, the most practical and helpful behavior would be:
Silently ignore / skip headers whose value is None (treating them as omitted/unset), or
Tolerate None values during tunnel header serialization so that unauthenticated proxy configurations do not break.
I'd appreciate it if you avoided responding via LLMs -- it's much more meaningful to use your own words!
Anyway, thanks for the repro. Has this been reported elsewhere? I can't find any reports of this on Thrift or CodeChecker's issue trackers. (We can probably fix it on 3.14; it's just a question of whether it's worth asking the RMs for backports to older versions, since this resulted from a security fix.)
Hi! I'm new to the CPython community and looking for a first issue to contribute to.. I think this one might be a good fit. What do you think? Is anyone already working on a fix?
Looking at the existing code in _tunnel(), the pattern for invalid headers already raises ValueError -- so I was thinking of adding a check before calling .encode() to raise ValueError when a header value is None.
Would that be the right approach, or would you prefer to silently skip None values for backwards compatibility?
IIRC, empty HTTP headers are technically valid under the spec.
I think both ignoring None values and the current error might have unintended consequences. I think a good middle ground is to send them empty and avoid calling .encode() in those cases.
Although I think this could require more discussion
Yeah, this needs more discussion, but feel free to submit a fix once we've reached consensus. I'd personally like to see some concrete examples of where this causes breakage; a minimal snippet using a third-party library that worked in 3.14.x and no longer works in 3.14.x+1 would be very convincing.
Return a different exception when people try to input a non-string value, more clearly explaining the issue (current traceback is not very intuitive on what to do next)
Attempt to convert values to strings (maybe have a special case where None becomes '')
Hey everyone! I did some digging and wanted to share what I found.
Root cause
This was introduced in #146212 (05ed7ce7ae9) as a fix for gh-146211 (HTTP CONNECT tunneling didn't sanitize CR/LF in headers). The fix was backported to 3.10–3.14. It replaced:
# before -- f-string coerces value via str() implicitlyheaders.append(f"{header}: {value}\r\n".encode("latin-1"))
with:
# after -- value must already be strheader_bytes=header.encode("latin-1")
value_bytes=value.encode("latin-1") # crashes if value is not a string
So any HTTPS request through an unauthenticated proxy ends up calling set_tunnel(..., {"Proxy-Authorization": None}), which crashes when _tunnel() calls None.encode("latin-1").
Before this change, the f-string silently coerced None to "None", so Thrift was sending Proxy-Authorization: None in the CONNECT.
That is a latent bug in Thrift -- for an unauthenticated proxy the header should not be sent -- but proxies tend to ignore unrecognised auth values, so it worked in practice.
Worth opening an issue on the Thrift side too, which would also answer @ZeroIntensity's question about whether this has been reported before or not.
Reproducing
@ZeroIntensity asked for a minimal snippet using a third-party library.
pip install thrift is the only dependency. This is reproducer.py:
importos, socket, ssl, threadingfromthrift.transportimportTHttpClientdefaccept_one(srv):
conn, _=srv.accept()
conn.recv(4096)
conn.sendall(b'HTTP/1.1 200 Connection established\r\n\r\n')
conn.recv(4096)
conn.close()
srv=socket.socket()
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(('127.0.0.1', 0))
srv.listen(1)
threading.Thread(target=accept_one, args=(srv,), daemon=True).start()
os.environ['https_proxy'] =f'http://127.0.0.1:{srv.getsockname()[1]}'transport=THttpClient.THttpClient('https://example.com/api')
transport.write(b'hello')
try:
transport.flush()
exceptAttributeErrorase:
print(f'REGRESSION: {e}')
exceptssl.SSLError:
# The fake server accepts the CONNECT and responds 200, but doesn't speak# SSL -- so the handshake fails here. This means _tunnel() succeeded and# the regression isn't present.print('OK -- tunnel established (SSL failed on fake server, expected)')
AttributeError: 'NoneType' object has no attribute 'encode'
3.13.13
OK
3.13.14
AttributeError: 'NoneType' object has no attribute 'encode'
3.14.4
OK
3.14.5
AttributeError: 'NoneType' object has no attribute 'encode'
3.15-rc (3.15.0rc3)
AttributeError: 'NoneType' object has no attribute 'encode'
Fix approaches and their impact
As @edvilme noted, this affects any non-str value. I tested four approaches on Python 3.12.14:
Skip None -- omit the header when the value is None. This is new behavior, but it seems semantically correct to me for the unauthenticated-proxy case.
ifvalueisNone: continue
Result: OK -- tunnel established
ValueError on None -- raise explicitly instead of crashing with AttributeError. Thrift still breaks, just with a clearer error.
ifvalueisNone: raiseValueError(...)
Result: ValueError: Invalid tunnel header value None for header 'Proxy-Authorization'
None => "" -- sends Proxy-Authorization: (empty value) in the CONNECT. It works, but it seems semantically wrong to me: we'd still be sending an auth header, but there are no credentials.
ifvalueisNone: value=""
Result: OK -- tunnel established
str(value) -- restores the previous behavior: None => "None", 42 => "42", any object => its __str__. This is the closest to the old behavior, but tools like Thrift would still keep sending the header Proxy-Authorization: None.
ifnotisinstance(value, str): value=str(value)
Result: OK -- tunnel established
What do you think? Happy to open a PR once there’s consensus on the approach :D
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:)
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
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.
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.
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.
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
Activity
edvilme commentedon Oct 9, 2026
According to https://devguide.python.org/versions/, Python 3.10 has reached EOL
ZeroIntensity commentedon Oct 9, 2026
Yes, Python 3.10 went EOL last week, so unfortunately, we can't change anything there. This does affect recent versions, though, since the regressing change (#146212) was backported to other versions, so we could potentially fix it in those.
That said,
_tunnelis a private method, so you opt in to these sorts of breakages by using it. Do you have any data on how widespread the breakage is here (not just libraries that passNoneto headers, but libraries that call_tunnel)? Or, ideally, a repro that doesn't use private APIs?edvilme commentedon Oct 9, 2026
Is the desired behavior to not raise any exception? I think passing
Nonequalifies as an "ilegal" header/value name and thus must raise the same error:gaio-kimura commentedon Oct 9, 2026
Thank you for the quick and helpful response!
1. Minimal Repro Using Public APIs (
connect())Here is a minimal reproduction using only public APIs (
set_tunnelandconnect). No private methods like_tunnel()are called directly:2. Real-World Context: Unauthenticated Proxies & Widespread Breakage
This issue breaks real-world CLI tools and applications such as Apache Thrift (
THttpClient).In practice, this commonly happens when configuring an HTTP proxy without user credentials via environment variables:
THttpClient.flush()) construct request/tunnel headers dynamically.None(e.g.{"Proxy-Authorization": None}or unset auth token headers) and pass the dictionary toset_tunnel(..., headers=...).conn.connect(), which internally calls_tunnel()and crashes with:AttributeError: 'NoneType' object has no attribute 'encode'Users encounter this failure simply by running standard CLI commands on unauthenticated proxy networks after upgrading Python.
3. Previous Behavior vs. Desired Behavior
Regarding whether
Noneshould raise aValueError:Previous Behavior (Python 3.10.20 and earlier):
Passing
Noneas a header value did not raise an exception. Requests and handshakes through unauthenticated proxies worked seamlessly across numerous releases.Current Behavior (Python 3.10.21):
The handshake immediately fails with
AttributeError: 'NoneType' object has no attribute 'encode'.Desired Behavior:
If Python strictly treats
Noneas an illegal value and raisesValueError: Invalid header name/value None, it will still break existing real-world libraries and proxy setups that previously worked without issue.For backwards compatibility, the most practical and helpful behavior would be:
None(treating them as omitted/unset), orNonevalues during tunnel header serialization so that unauthenticated proxy configurations do not break.ZeroIntensity commentedon Oct 9, 2026
I'd appreciate it if you avoided responding via LLMs -- it's much more meaningful to use your own words!
Anyway, thanks for the repro. Has this been reported elsewhere? I can't find any reports of this on Thrift or CodeChecker's issue trackers. (We can probably fix it on 3.14; it's just a question of whether it's worth asking the RMs for backports to older versions, since this resulted from a security fix.)
bielpieroli commentedon Oct 9, 2026
Hi! I'm new to the CPython community and looking for a first issue to contribute to.. I think this one might be a good fit. What do you think? Is anyone already working on a fix?
Looking at the existing code in _tunnel(), the pattern for invalid headers already raises ValueError -- so I was thinking of adding a check before calling .encode() to raise ValueError when a header value is None.
Would that be the right approach, or would you prefer to silently skip None values for backwards compatibility?
edvilme commentedon Oct 9, 2026
IIRC, empty HTTP headers are technically valid under the spec.
I think both ignoring
Nonevalues and the current error might have unintended consequences. I think a good middle ground is to send them empty and avoid calling.encode()in those cases.Although I think this could require more discussion
ZeroIntensity commentedon Oct 10, 2026
Yeah, this needs more discussion, but feel free to submit a fix once we've reached consensus. I'd personally like to see some concrete examples of where this causes breakage; a minimal snippet using a third-party library that worked in 3.14.x and no longer works in 3.14.x+1 would be very convincing.
edvilme commentedon Oct 10, 2026
After a bit more exploration, I found that setting any non-string value causes the same issue:
I think we could:
NoneheadersNonebecomes'')bielpieroli commentedon Oct 10, 2026
Hey everyone! I did some digging and wanted to share what I found.
Root cause
This was introduced in #146212 (
05ed7ce7ae9) as a fix for gh-146211 (HTTPCONNECTtunneling didn't sanitize CR/LF in headers). The fix was backported to 3.10–3.14. It replaced:with:
Apache Thrift with unauthenticated proxy
Here’s the full code:
thrift/transport/THttpClient.py:__init__--proxy_authis set frombasic_proxy_auth_header():basic_proxy_auth_header-- returnsNonewhen the proxy has no credentials:open-- passesself.proxy_auth(nowNone) directly toset_tunnel:So any HTTPS request through an unauthenticated proxy ends up calling
set_tunnel(..., {"Proxy-Authorization": None}), which crashes when_tunnel()callsNone.encode("latin-1").Before this change, the f-string silently coerced
Noneto"None", so Thrift was sendingProxy-Authorization: Nonein the CONNECT.That is a latent bug in Thrift -- for an unauthenticated proxy the header should not be sent -- but proxies tend to ignore unrecognised auth values, so it worked in practice.
Worth opening an issue on the Thrift side too, which would also answer @ZeroIntensity's question about whether this has been reported before or not.
Reproducing
@ZeroIntensity asked for a minimal snippet using a third-party library.
pip install thriftis the only dependency. This isreproducer.py:Tested across versions with Docker:
AttributeError: 'NoneType' object has no attribute 'encode'AttributeError: 'NoneType' object has no attribute 'encode'AttributeError: 'NoneType' object has no attribute 'encode'AttributeError: 'NoneType' object has no attribute 'encode'Fix approaches and their impact
As @edvilme noted, this affects any non-
strvalue. I tested four approaches on Python 3.12.14:Skip
None-- omit the header when the value isNone. This is new behavior, but it seems semantically correct to me for the unauthenticated-proxy case.Result: OK -- tunnel established
ValueErroronNone-- raise explicitly instead of crashing withAttributeError. Thrift still breaks, just with a clearer error.Result:
ValueError: Invalid tunnel header value None for header 'Proxy-Authorization'None=>""-- sendsProxy-Authorization:(empty value) in the CONNECT. It works, but it seems semantically wrong to me: we'd still be sending an auth header, but there are no credentials.Result: OK -- tunnel established
str(value)-- restores the previous behavior:None=>"None",42=>"42", any object => its__str__. This is the closest to the old behavior, but tools like Thrift would still keep sending the headerProxy-Authorization: None.Result: OK -- tunnel established
What do you think? Happy to open a PR once there’s consensus on the approach :D
edvilme commentedon Oct 10, 2026
I think we should convert to str either way
As for the
Nonevalue, 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:)
bielpieroli commentedon Oct 10, 2026
Agreed on converting to
strin general, that restores the old behavior for ints and other objects.For
NoneI'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 howrequeststreats a header set toNone.It would be simple: skip the header if the value is
None, otherwisestr(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
LorenaMBorges commentedon Oct 10, 2026
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.
LorenaMBorges commentedon Oct 10, 2026
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.
ZeroIntensity commentedon Oct 11, 2026
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:It'd also be nice if we explicitly documented that the headers are converted to a string.
pfmoore commentedon Oct 11, 2026
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.edvilme commentedon Oct 11, 2026
Calling
.connect()with tunnels set internally calls the private method, so the bug is present in regular case scenarios and even with 3rd party librariespythongh-159044: Fix http.client tunnel headers with non-string values
pythongh-159044: Fix http.client tunnel headers with non-string values
ZeroIntensity commentedon Oct 12, 2026
I had Codex track down libraries that are affected and I came up with a script to reproduce it:
Details
So, concretely, (at least) all of the following are affected:
But, I couldn't find any reports on their issue trackers, so I guess this has just gone unnoticed so far.
gh-159044: Restore string conversion for HTTP tunnel header values (G…
[3.14] gh-159044: Restore string conversion for HTTP tunnel header va…
[3.15] gh-159044: Restore string conversion for HTTP tunnel header va…