Repository navigation
Headers.encoding is cached from the raw bytes and never invalidated, so non-ASCII writes and reads fail after a read #3808
Replies: 1 comment
|
Same result here on httpx 0.28.1 (python:3.13-slim). Both of your snippets fail exactly as shown. The cached import httpx
def handler(request):
return httpx.Response(200)
client = httpx.Client(headers={"Custom": "Example"}, transport=httpx.MockTransport(handler))
client.get("http://t/")
client.headers["X"] = "☃"
# UnicodeEncodeError: 'ascii' codec can't encode character '☃' in position 0
def hook(request):
if "authorization" not in request.headers: # __contains__ caches 'ascii'
request.headers["X-Name"] = "José"
httpx.Client(transport=httpx.MockTransport(handler), event_hooks={"request": [hook]}).get("http://t/")
# UnicodeEncodeError: 'ascii' codec can't encode character '\xe9' in position 3Until a fix lands, pinning the encoding explicitly avoids it, because then nothing is left for the lazy property to cache: headers = httpx.Headers({"Custom": "Example"}, encoding="utf-8")
# or, on an existing object (client.headers, request.headers in a hook):
request.headers.encoding = "utf-8"With the encoding pinned, both of your snippets and the event hook above work. Setting the encoding after a bytes One thing to keep in mind about the workaround. With |
Uh oh!
There was an error while loading. Please reload this page.
Headersusesself._encodingfor two different things — the explicitly configured encoding, and the cache for the encoding it lazily determines from the raw header bytes — and never invalidates that cache when the headers change. That makes two valid operations fail depending on access history:Byte keys and values are a supported
HeaderTypes, and the utf-8/iso-8859-1 fallback in theencodingproperty exists exactly for them, so an object whose ownrawis correct should not be unable to read it back.copy()also pins the determined encoding onto the copy, so the same gets inherited.Where it comes from, in the current code:
httpx/_models.py:183— theencodingproperty writes the determined encoding back intoself._encoding.httpx/_models.py:309-310—__setitem__encodes withself._encoding or "utf-8", i.e. the fallback only works while_encodingis stillNone.httpx/_models.py:279—update()first caches the pre-update encoding of the existing headers throughkey in self→__contains__→self.encoding.httpx/_models.py:282—copy()carries the determined value over.What I'd expect:
_encodingkeeps only an explicitly configured encoding, the determined one is cached separately, and the determined value is re-computed afterupdate(),__setitem__and__delitem__.I have a fix for this with a regression test for both cases, and can open a pull request if you'd consider it.
All reactions