httpx
httpx copied to clipboard
The code in the document does not work
Hi. The code in the document does not work
import httpx
files = {'upload-file': (None, 'text content', 'text/plain')}
r = httpx.post("https://httpbin.org/post", files=files)
print(r.text)
The error is
TypeError: Expected bytes or bytes-like object got: <class 'str'>
I found this problem has been raised but not solved
If the string is modified with b, it works fine
import httpx
files = {'upload-file': (None, b'text content', 'text/plain')}
r = httpx.post("https://httpbin.org/post", files=files)
print(r.text)
Compared to requests
import requests
import httpx
files = {'upload-file': (None, 'text content', 'text/plain')}
httpx.post("https://httpbin.org/post", files=files) # error
requests.post("https://httpbin.org/post", files=files) # works
files = {'upload-file': (None, b'text content', 'text/plain')}
httpx.post("https://httpbin.org/post", files=files) # works
requests.post("https://httpbin.org/post", files=files) # works
Thanks for raising this @imsgj - you're absolutely correct.
We made a change here so that only binary content is accepted. However I'm not sure we really want to do that or not. The necessary case was that we only accept files opened in binary mode. But we could still accept the plain ol' str
case if we wanted to.
We've got two options here.
- Accept that we've got sensible behaviour, and update the docs to match the expected usage.
- Change the behaviour so that plain
str
cases are accepted. (But files opened in text mode are not.)
If we decide to keep the behavior -- should we also try to sniff "have we got str content?" early on enough so that we display a friendlier error message, also explaining why we only accept bytes
? I don't know where the TypeError
comes from, probably not from us directly. I assume it might be a frequent enough attempt to pass str
here assuming it would be accepted.
FWIW I am hopeful that we can allow non-files for two reasons:
- It would be consistent with
requests
- I just ran into exactly this issue because the Cloudflare Images API recently fixed a long-standing issue, but in doing so changed to require
multipart/form-data
requests (see https://developers.cloudflare.com/images/cloudflare-images/upload-images/direct-creator-upload/). This used to take urlencoded JSON, so my client instantly broke. I worked around it by faking the files argument with an empty pydantic instance:
async with httpx.AsyncClient() as client:
cloudflare_response = await client.post(
url=url,
data=data_dict,
files=pydantic.BaseModel(), # truthy+iterable+empty to fake httpx into doing multipart encoding for v2 API without actually sending any files
headers={
# 'Content-Type': application/json', # only used for v1 of the API
'Authorization': f"Bearer {api_token}"
})
FWIW I am hopeful that we can allow non-files for two reasons:
- It would be consistent with
requests
- I just ran into exactly this issue because the Cloudflare Images API recently fixed a long-standing issue, but in doing so changed to require
multipart/form-data
requests (see https://developers.cloudflare.com/images/cloudflare-images/upload-images/direct-creator-upload/). This used to take urlencoded JSON, so my client instantly broke. I worked around it by faking the files argument with an empty pydantic instance:async with httpx.AsyncClient() as client: cloudflare_response = await client.post( url=url, data=data_dict, files=pydantic.BaseModel(), # truthy+iterable+empty to fake httpx into doing multipart encoding for v2 API without actually sending any files headers={ # 'Content-Type': application/json', # only used for v1 of the API 'Authorization': f"Bearer {api_token}" })
You can use :
payload = (
('foo', (None, b'bar')),
('variable', (None, str.encode(variable))),
('bar', (None, b'foo')),
)
client.post("https://.../api/post", headers=headers, files=payload)
This will work.
The other problem that I have is that we are not able to set a "WebKitFormBoundary" for the form, this could be really useful because almost every website using multipart are using a boundary