Repository: encode/httpx

Issue:
Sync client fails on ASGI apps with unhelpful traceback
I tried using httpx with with an ASGI app in the way that was implied by the docs.  It throws an error that feels like a problem with httpx, as FastAPI is currently being used in production in other ways.  If you confirm that this is a bug in httpx, I'm willing (and probably capable) to fix it and submit a patch.

```
import httpx
from fastapi import FastAPI

app = FastAPI()


@app.get("/users/{user_id}")
async def read_user(user_id: str):
    return {'user_id': user_id}

with httpx.Client(app=app) as client:
    response = client.get("http://www.example.com/users/foo")
```

Throws the error:
```
Traceback (most recent call last):
  File "temp.py", line 12, in <module>
    response = client.get("http://www.example.com/users/foo")
  File "/Users/singletoned/.envs/testino/lib/python3.7/site-packages/httpx/_client.py", line 714, in get
    timeout=timeout,
  File "/Users/singletoned/.envs/testino/lib/python3.7/site-packages/httpx/_client.py", line 571, in request
    request, auth=auth, allow_redirects=allow_redirects, timeout=timeout,
  File "/Users/singletoned/.envs/testino/lib/python3.7/site-packages/httpx/_client.py", line 591, in send
    request, auth=auth, timeout=timeout, allow_redirects=allow_redirects,
  File "/Users/singletoned/.envs/testino/lib/python3.7/site-packages/httpx/_client.py", line 618, in send_handling_redirects
    request, auth=auth, timeout=timeout, history=history
  File "/Users/singletoned/.envs/testino/lib/python3.7/site-packages/httpx/_client.py", line 654, in send_handling_auth
    response = self.send_single_request(request, timeout)
  File "/Users/singletoned/.envs/testino/lib/python3.7/site-packages/httpx/_client.py", line 678, in send_single_request
    response = dispatcher.send(request, timeout=timeout)
  File "/Users/singletoned/.envs/testino/lib/python3.7/site-packages/httpx/_dispatch/wsgi.py", line 90, in send
    result = self.app(environ, start_response)
TypeError: __call__() missing 1 required positional argument: 'send'
```

Discussion:

Patch:
diff --git a/docs/api.md b/docs/api.md
index 3889d8c..c63d4ec 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -38,6 +38,13 @@
     :docstring:
     :members: headers cookies params request get head options post put patch delete build_request send close
 
+## `AsyncClient`
+
+::: httpx.AsyncClient
+    :docstring:
+    :members: headers cookies params request get head options post put patch delete build_request send
+
+
 ## `Response`
 
 *An HTTP response.*
diff --git a/httpx/_client.py b/httpx/_client.py
index 2a310e2..3976522 100644
--- a/httpx/_client.py
+++ b/httpx/_client.py
@@ -426,7 +426,7 @@ class Client(BaseClient):
     request URLs.
     * **dispatch** - *(optional)* A dispatch class to use for sending requests
     over the network.
-    * **app** - *(optional)* An ASGI application to send requests to,
+    * **app** - *(optional)* An WSGI application to send requests to,
     rather than sending actual network requests.
     * **trust_env** - *(optional)* Enables or disables usage of environment
     variables for configuration.
diff --git a/httpx/_content_streams.py b/httpx/_content_streams.py
index afffed3..dd6c51f 100644
--- a/httpx/_content_streams.py
+++ b/httpx/_content_streams.py
@@ -71,8 +71,6 @@ class ByteStream(ContentStream):
         self.body = body.encode("utf-8") if isinstance(body, str) else body
 
     def get_headers(self) -> typing.Dict[str, str]:
-        if not self.body:
-            return {}
         content_length = str(len(self.body))
         return {"Content-Length": content_length}
 


Test patch:
diff --git a/tests/client/test_headers.py b/tests/client/test_headers.py
index 22b0ba1..5c4dbb4 100755
--- a/tests/client/test_headers.py
+++ b/tests/client/test_headers.py
@@ -40,6 +40,7 @@ async def test_client_header():
             "accept": "*/*",
             "accept-encoding": "gzip, deflate, br",
             "connection": "keep-alive",
+            "content-length": "0",
             "example-header": "example-value",
             "host": "example.org",
             "user-agent": f"python-httpx/{__version__}",
@@ -61,6 +62,7 @@ async def test_header_merge():
             "accept": "*/*",
             "accept-encoding": "gzip, deflate, br",
             "connection": "keep-alive",
+            "content-length": "0",
             "host": "example.org",
             "user-agent": "python-myclient/0.2.1",
             "x-auth-token": "FooBarBazToken",
@@ -82,6 +84,7 @@ async def test_header_merge_conflicting_headers():
             "accept": "*/*",
             "accept-encoding": "gzip, deflate, br",
             "connection": "keep-alive",
+            "content-length": "0",
             "host": "example.org",
             "user-agent": f"python-httpx/{__version__}",
             "x-auth-token": "BazToken",
@@ -105,6 +108,7 @@ async def test_header_update():
             "accept": "*/*",
             "accept-encoding": "gzip, deflate, br",
             "connection": "keep-alive",
+            "content-length": "0",
             "host": "example.org",
             "user-agent": f"python-httpx/{__version__}",
         }
@@ -117,6 +121,7 @@ async def test_header_update():
             "accept-encoding": "gzip, deflate, br",
             "another-header": "AThing",
             "connection": "keep-alive",
+            "content-length": "0",
             "host": "example.org",
             "user-agent": "python-myclient/0.2.1",
         }
@@ -147,6 +152,7 @@ async def test_host_with_auth_and_port_in_url():
             "accept": "*/*",
             "accept-encoding": "gzip, deflate, br",
             "connection": "keep-alive",
+            "content-length": "0",
             "host": "example.org",
             "user-agent": f"python-httpx/{__version__}",
             "authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
@@ -171,6 +177,7 @@ async def test_host_with_non_default_port_in_url():
             "accept": "*/*",
             "accept-encoding": "gzip, deflate, br",
             "connection": "keep-alive",
+            "content-length": "0",
             "host": "example.org:123",
             "user-agent": f"python-httpx/{__version__}",
             "authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
diff --git a/tests/client/test_redirects.py b/tests/client/test_redirects.py
index 190ff09..63403a6 100644
--- a/tests/client/test_redirects.py
+++ b/tests/client/test_redirects.py
@@ -279,7 +279,7 @@ async def test_body_redirect():
     response = await client.post(url, data=data)
     assert response.url == URL("https://example.org/redirect_body_target")
     assert response.json()["body"] == "Example request body"
-    assert "content-length" in response.json()["headers"]
+    assert response.json()["headers"]["content-length"] == "20"
 
 
 @pytest.mark.usefixtures("async_environment")
@@ -293,7 +293,7 @@ async def test_no_body_redirect():
     response = await client.post(url, data=data)
     assert response.url == URL("https://example.org/redirect_body_target")
     assert response.json()["body"] == ""
-    assert "content-length" not in response.json()["headers"]
+    assert response.json()["headers"]["content-length"] == "0"
 
 
 @pytest.mark.usefixtures("async_environment")
diff --git a/tests/models/test_requests.py b/tests/models/test_requests.py
index c72e0af..0461394 100644
--- a/tests/models/test_requests.py
+++ b/tests/models/test_requests.py
@@ -10,7 +10,7 @@ def test_request_repr():
 
 def test_no_content():
     request = httpx.Request("GET", "http://example.org")
-    assert "Content-Length" not in request.headers
+    assert request.headers["Content-Length"] == "0"
 
 
 def test_content_length_header():
diff --git a/tests/test_content_streams.py b/tests/test_content_streams.py
index c5eb961..84999ef 100644
--- a/tests/test_content_streams.py
+++ b/tests/test_content_streams.py
@@ -25,7 +25,9 @@ async def test_empty_content():
     async_content = b"".join([part async for part in stream])
 
     assert stream.can_replay()
-    assert stream.get_headers() == {}
+    assert stream.get_headers() == {
+        "Content-Length": "0",
+    }
     assert sync_content == b""
     assert async_content == b""
 
@@ -196,7 +198,9 @@ async def test_empty_request():
     async_content = b"".join([part async for part in stream])
 
     assert stream.can_replay()
-    assert stream.get_headers() == {}
+    assert stream.get_headers() == {
+        "Content-Length": "0",
+    }
     assert sync_content == b""
     assert async_content == b""