Repository: encode/httpx

Issue:
Redundant `boundary` if files/data is empty
If you try to send a post request with empty data and files — you will create a body with one `--boundary--` string, which can break the server.

It's because httpx create MultipartStream for `data={}, files={}`:
https://github.com/encode/httpx/blob/a82adcc933345c6b8cb1623b031eb85723e7665b/httpx/_content_streams.py#L332-L334

And this MultipartStream doesn't have special behavior for empty data/files:
https://github.com/encode/httpx/blob/a82adcc933345c6b8cb1623b031eb85723e7665b/httpx/_content_streams.py#L277-L283

Reproducible example:

```
from httpx.content_streams import encode

stream = encode(data={}, files={}, boundary=b"+++")
print(b"".join([part for part in stream]))
# b'--+++--\r\n'
```

I see two ways to solve it:
1. Check files not only for None but also for emptiness in `_content_streams.decode`
2. Check the count of fields in `MultipartStream.__init__` and doesn't write anything if fields are empty.

I already opened PR (#861) with a test example. If someone tells me which of these options is better, I will be happy to implement it  🙌

Discussion:
florimondmanca: Looks like we've got some unrelated type checking breakage here, issued #865 to fix it. :-)
florimondmanca: Merged `master` in here, should resolve the build. :-)

Patch:
diff --git a/httpx/_content_streams.py b/httpx/_content_streams.py
index 5f3237e..afffed3 100644
--- a/httpx/_content_streams.py
+++ b/httpx/_content_streams.py
@@ -323,7 +323,7 @@ def encode(
     Handles encoding the given `data`, `files`, and `json`, returning
     a `ContentStream` implementation.
     """
-    if data is None:
+    if not data:
         if json is not None:
             return JSONStream(json=json)
         elif files:
@@ -331,7 +331,7 @@ def encode(
         else:
             return ByteStream(body=b"")
     elif isinstance(data, dict):
-        if files is not None:
+        if files:
             return MultipartStream(data=data, files=files, boundary=boundary)
         else:
             return URLEncodedStream(data=data)


Test patch:
diff --git a/tests/test_content_streams.py b/tests/test_content_streams.py
index 6aa91f4..c5eb961 100644
--- a/tests/test_content_streams.py
+++ b/tests/test_content_streams.py
@@ -189,6 +189,18 @@ async def test_multipart_data_and_files_content():
     )
 
 
+@pytest.mark.asyncio
+async def test_empty_request():
+    stream = encode(data={}, files={})
+    sync_content = b"".join([part for part in stream])
+    async_content = b"".join([part async for part in stream])
+
+    assert stream.can_replay()
+    assert stream.get_headers() == {}
+    assert sync_content == b""
+    assert async_content == b""
+
+
 def test_invalid_argument():
     with pytest.raises(TypeError):
         encode(123)