httpx icon indicating copy to clipboard operation
httpx copied to clipboard

The code in the document does not work

Open imsgj opened this issue 3 years ago • 4 comments

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

imsgj avatar Feb 05 '22 07:02 imsgj

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.)

tomchristie avatar Feb 09 '22 11:02 tomchristie

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.

florimondmanca avatar Feb 26 '22 11:02 florimondmanca

FWIW I am hopeful that we can allow non-files for two reasons:

  1. It would be consistent with requests
  2. 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}"
        })

jamesboehmer avatar Mar 03 '22 19:03 jamesboehmer

FWIW I am hopeful that we can allow non-files for two reasons:

  1. It would be consistent with requests
  2. 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

ImBatou avatar Jun 20 '22 16:06 ImBatou