Accessing `response.url` will return a `URL` instance, rather than a string.
Use `str(response.url)` if you need a string instance.
+## Request Content
+
+For uploading raw text or binary content we prefer to use a `content` parameter,
+in order to better separate this usage from the case of uploading form data.
+
+For example, using `content=...` to upload raw content:
+
+```python
+# Uploading text, bytes, or a bytes iterator.
+httpx.post(..., content=b"Hello, world")
+```
+
+And using `data=...` to send form data:
+
+```python
+# Uploading form data.
+httpx.post(..., data={"message": "Hello, world"})
+```
+
+If you're using a type checking tool such as `mypy`, you'll see warnings issues if using test/byte content with the `data` argument.
+However, for compatibility reasons with `requests`, we do still handle the case where `data=...` is used with raw binary and text contents.
+
## Status Codes
In our documentation we prefer the uppercased versions, such as `codes.NOT_FOUND`, but also provide lower-cased versions for API compatibility with `requests`.
## Sending Binary Request Data
-For other encodings, you should use either a `bytes` type or a generator
-that yields `bytes`.
+For other encodings, you should use the `content=...` parameter, passing
+either a `bytes` type or a generator that yields `bytes`.
-You'll probably also want to set a custom `Content-Type` header when uploading
+```pycon
+>>> content = b'Hello, world'
+>>> r = httpx.post("https://httpbin.org/post", content=content)
+```
+
+You may also want to set a custom `Content-Type` header when uploading
binary data.
-## Response Status Codes
+## Response Status Codes
We can inspect the HTTP status code of the response:
HeaderTypes,
ProxiesTypes,
QueryParamTypes,
+ RequestContent,
RequestData,
RequestFiles,
TimeoutTypes,
url: URLTypes,
*,
params: QueryParamTypes = None,
+ content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
* **url** - URL for the new `Request` object.
* **params** - *(optional)* Query parameters to include in the URL, as a
string, dictionary, or list of two-tuples.
- * **data** - *(optional)* Data to include in the body of the request, as a
- dictionary
+ * **content** - *(optional)* Binary content to include in the body of the
+ request, as bytes or a byte iterator.
+ * **data** - *(optional)* Form data to include in the body of the request,
+ as a dictionary.
* **files** - *(optional)* A dictionary of upload files to include in the
body of the request.
* **json** - *(optional)* A JSON serializable object to include in the body
return client.request(
method=method,
url=url,
+ content=content,
data=data,
files=files,
json=json,
url: URLTypes,
*,
params: QueryParamTypes = None,
+ content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
method=method,
url=url,
params=params,
+ content=content,
data=data,
files=files,
json=json,
def post(
url: URLTypes,
*,
+ content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
return request(
"POST",
url,
+ content=content,
data=data,
files=files,
json=json,
def put(
url: URLTypes,
*,
+ content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
return request(
"PUT",
url,
+ content=content,
data=data,
files=files,
json=json,
def patch(
url: URLTypes,
*,
+ content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
return request(
"PATCH",
url,
+ content=content,
data=data,
files=files,
json=json,
HeaderTypes,
ProxiesTypes,
QueryParamTypes,
+ RequestContent,
RequestData,
RequestFiles,
TimeoutTypes,
method: str,
url: URLTypes,
*,
+ content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
request = self.build_request(
method=method,
url=url,
+ content=content,
data=data,
files=files,
json=json,
method: str,
url: URLTypes,
*,
+ content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
return Request(
method,
url,
+ content=content,
data=data,
files=files,
json=json,
method: str,
url: URLTypes,
*,
+ content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
request = self.build_request(
method=method,
url=url,
+ content=content,
data=data,
files=files,
json=json,
self,
url: URLTypes,
*,
+ content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
return self.request(
"POST",
url,
+ content=content,
data=data,
files=files,
json=json,
self,
url: URLTypes,
*,
+ content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
return self.request(
"PUT",
url,
+ content=content,
data=data,
files=files,
json=json,
self,
url: URLTypes,
*,
+ content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
return self.request(
"PATCH",
url,
+ content=content,
data=data,
files=files,
json=json,
method: str,
url: URLTypes,
*,
+ content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
request = self.build_request(
method=method,
url=url,
+ content=content,
data=data,
files=files,
json=json,
self,
url: URLTypes,
*,
+ content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
return await self.request(
"POST",
url,
+ content=content,
data=data,
files=files,
json=json,
self,
url: URLTypes,
*,
+ content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
return await self.request(
"PUT",
url,
+ content=content,
data=data,
files=files,
json=json,
self,
url: URLTypes,
*,
+ content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
return await self.request(
"PATCH",
url,
+ content=content,
data=data,
files=files,
json=json,
import httpcore
from ._exceptions import StreamConsumed
-from ._types import FileContent, FileTypes, RequestData, RequestFiles, ResponseContent
+from ._types import (
+ FileContent,
+ FileTypes,
+ RequestContent,
+ RequestData,
+ RequestFiles,
+ ResponseContent,
+)
from ._utils import (
format_form_param,
guess_content_type,
def encode(
+ content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
boundary: bytes = None,
) -> ContentStream:
"""
- Handles encoding the given `data`, `files`, and `json`, returning
- a `ContentStream` implementation.
+ Handles encoding the given `content`, `data`, `files`, and `json`,
+ returning a `ContentStream` implementation.
"""
- if not data:
- if json is not None:
- return JSONStream(json=json)
- elif files:
- return MultipartStream(data={}, files=files, boundary=boundary)
+ if data is not None and not isinstance(data, dict):
+ # We prefer to seperate `content=<bytes|byte iterator|bytes aiterator>`
+ # for raw request content, and `data=<form data>` for url encoded or
+ # multipart form content.
+ #
+ # However for compat with requests, we *do* still support
+ # `data=<bytes...>` usages. We deal with that case here, treating it
+ # as if `content=<...>` had been supplied instead.
+ content = data
+ data = None
+
+ if content is not None:
+ if isinstance(content, (str, bytes)):
+ return ByteStream(body=content)
+ elif hasattr(content, "__aiter__"):
+ content = typing.cast(typing.AsyncIterator[bytes], content)
+ return AsyncIteratorStream(aiterator=content)
+ elif hasattr(content, "__iter__"):
+ content = typing.cast(typing.Iterator[bytes], content)
+ return IteratorStream(iterator=content)
else:
- return ByteStream(body=b"")
- elif isinstance(data, dict):
+ raise TypeError(f"Unexpected type for 'content', {type(content)!r}")
+
+ elif data:
if files:
return MultipartStream(data=data, files=files, boundary=boundary)
else:
return URLEncodedStream(data=data)
- elif isinstance(data, (str, bytes)):
- return ByteStream(body=data)
- elif isinstance(data, typing.AsyncIterator):
- return AsyncIteratorStream(aiterator=data)
- elif isinstance(data, typing.Iterator):
- return IteratorStream(iterator=data)
-
- raise TypeError(f"Unexpected type for 'data', {type(data)!r}")
+
+ elif files:
+ return MultipartStream(data={}, files=files, boundary=boundary)
+
+ elif json is not None:
+ return JSONStream(json=json)
+
+ return ByteStream(body=b"")
def encode_response(content: ResponseContent = None) -> ContentStream:
HeaderTypes,
PrimitiveData,
QueryParamTypes,
+ RequestContent,
RequestData,
RequestFiles,
ResponseContent,
params: QueryParamTypes = None,
headers: HeaderTypes = None,
cookies: CookieTypes = None,
+ content: RequestContent = None,
data: RequestData = None,
files: RequestFiles = None,
json: typing.Any = None,
if stream is not None:
self.stream = stream
else:
- self.stream = encode(data, files, json)
+ self.stream = encode(content, data, files, json)
self._prepare()
None,
]
+RequestContent = Union[str, bytes, Iterator[bytes], AsyncIterator[bytes]]
ResponseContent = Union[bytes, Iterator[bytes], AsyncIterator[bytes]]
-RequestData = Union[dict, str, bytes, Iterator[bytes], AsyncIterator[bytes]]
+RequestData = dict
FileContent = Union[IO[str], IO[bytes], str, bytes]
FileTypes = Union[
async def test_post(server):
url = server.url
async with httpx.AsyncClient() as client:
- response = await client.post(url, data=b"Hello, world!")
+ response = await client.post(url, content=b"Hello, world!")
assert response.status_code == 200
yield b"world!"
async with httpx.AsyncClient() as client:
- response = await client.request("POST", server.url, data=hello_world())
+ response = await client.request("POST", server.url, content=hello_world())
assert response.status_code == 200
@pytest.mark.usefixtures("async_environment")
async def test_put(server):
async with httpx.AsyncClient() as client:
- response = await client.put(server.url, data=b"Hello, world!")
+ response = await client.put(server.url, content=b"Hello, world!")
assert response.status_code == 200
@pytest.mark.usefixtures("async_environment")
async def test_patch(server):
async with httpx.AsyncClient() as client:
- response = await client.patch(server.url, data=b"Hello, world!")
+ response = await client.patch(server.url, content=b"Hello, world!")
assert response.status_code == 200
@pytest.mark.usefixtures("async_environment")
async def test_100_continue(server):
headers = {"Expect": "100-continue"}
- data = b"Echo request body"
+ content = b"Echo request body"
async with httpx.AsyncClient() as client:
response = await client.post(
- server.url.copy_with(path="/echo_body"), headers=headers, data=data
+ server.url.copy_with(path="/echo_body"), headers=headers, content=content
)
assert response.status_code == 200
- assert response.content == data
+ assert response.content == content
@pytest.mark.usefixtures("async_environment")
def test_post(server):
with httpx.Client() as client:
- response = client.post(server.url, data=b"Hello, world!")
+ response = client.post(server.url, content=b"Hello, world!")
assert response.status_code == 200
assert response.reason_phrase == "OK"
def test_put(server):
with httpx.Client() as client:
- response = client.put(server.url, data=b"Hello, world!")
+ response = client.put(server.url, content=b"Hello, world!")
assert response.status_code == 200
assert response.reason_phrase == "OK"
def test_patch(server):
with httpx.Client() as client:
- response = client.patch(server.url, data=b"Hello, world!")
+ response = client.patch(server.url, content=b"Hello, world!")
assert response.status_code == 200
assert response.reason_phrase == "OK"
"""
client = httpx.Client(transport=MockTransport(redirects))
url = "https://example.org/redirect_body"
- data = b"Example request body"
- response = client.post(url, data=data)
+ content = b"Example request body"
+ response = client.post(url, content=content)
assert response.url == "https://example.org/redirect_body_target"
assert response.json()["body"] == "Example request body"
assert "content-length" in response.json()["headers"]
"""
client = httpx.Client(transport=MockTransport(redirects))
url = "https://example.org/redirect_no_body"
- data = b"Example request body"
- response = client.post(url, data=data)
+ content = b"Example request body"
+ response = client.post(url, content=content)
assert response.url == "https://example.org/redirect_body_target"
assert response.json()["body"] == ""
assert "content-length" not in response.json()["headers"]
yield b"Example request body" # pragma: nocover
with pytest.raises(httpx.RequestBodyUnavailable):
- client.post(url, data=streaming_body())
+ client.post(url, content=streaming_body())
def test_cross_subdomain_redirect():
def test_content_length_header():
- request = httpx.Request("POST", "http://example.org", data=b"test 123")
+ request = httpx.Request("POST", "http://example.org", content=b"test 123")
assert request.headers["Content-Length"] == "8"
def test_post(server):
- response = httpx.post(server.url, data=b"Hello, world!")
+ response = httpx.post(server.url, content=b"Hello, world!")
assert response.status_code == 200
assert response.reason_phrase == "OK"
yield b", "
yield b"world!"
- response = httpx.post(server.url, data=data())
+ response = httpx.post(server.url, content=data())
assert response.status_code == 200
assert response.reason_phrase == "OK"
def test_put(server):
- response = httpx.put(server.url, data=b"Hello, world!")
+ response = httpx.put(server.url, content=b"Hello, world!")
assert response.status_code == 200
assert response.reason_phrase == "OK"
def test_patch(server):
- response = httpx.patch(server.url, data=b"Hello, world!")
+ response = httpx.patch(server.url, content=b"Hello, world!")
assert response.status_code == 200
assert response.reason_phrase == "OK"
@pytest.mark.usefixtures("async_environment")
async def test_asgi_upload():
async with httpx.AsyncClient(app=echo_body) as client:
- response = await client.post("http://www.example.org/", data=b"example")
+ response = await client.post("http://www.example.org/", content=b"example")
assert response.status_code == 200
assert response.text == "example"
disconnect = message.get("type") == "http.disconnect"
async with httpx.AsyncClient(app=read_body) as client:
- response = await client.post("http://www.example.org/", data=b"example")
+ response = await client.post("http://www.example.org/", content=b"example")
assert response.status_code == 200
assert disconnect
@pytest.mark.asyncio
async def test_bytes_content():
- stream = encode(data=b"Hello, world!")
+ stream = encode(content=b"Hello, world!")
+ 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() == {"Content-Length": "13"}
+ assert sync_content == b"Hello, world!"
+ assert async_content == b"Hello, world!"
+
+ # Support 'data' for compat with requests.
+ stream = encode(data=b"Hello, world!") # type: ignore
sync_content = b"".join([part for part in stream])
async_content = b"".join([part async for part in stream])
yield b"Hello, "
yield b"world!"
- stream = encode(data=hello_world())
+ stream = encode(content=hello_world())
content = b"".join([part for part in stream])
assert not stream.can_replay()
with pytest.raises(StreamConsumed):
[part for part in stream]
+ # Support 'data' for compat with requests.
+ stream = encode(data=hello_world()) # type: ignore
+ content = b"".join([part for part in stream])
+
+ assert not stream.can_replay()
+ assert stream.get_headers() == {"Transfer-Encoding": "chunked"}
+ assert content == b"Hello, world!"
+
@pytest.mark.asyncio
async def test_aiterator_content():
yield b"Hello, "
yield b"world!"
- stream = encode(data=hello_world())
+ stream = encode(content=hello_world())
content = b"".join([part async for part in stream])
assert not stream.can_replay()
with pytest.raises(StreamConsumed):
[part async for part in stream]
+ # Support 'data' for compat with requests.
+ stream = encode(data=hello_world()) # type: ignore
+ content = b"".join([part async for part in stream])
+
+ assert not stream.can_replay()
+ assert stream.get_headers() == {"Transfer-Encoding": "chunked"}
+ assert content == b"Hello, world!"
+
@pytest.mark.asyncio
async def test_json_content():
async with httpx.AsyncClient(timeout=timeout) as client:
with pytest.raises(httpx.WriteTimeout):
data = b"*" * 1024 * 1024 * 100
- await client.put(server.url.copy_with(path="/slow_response"), data=data)
+ await client.put(server.url.copy_with(path="/slow_response"), content=data)
@pytest.mark.usefixtures("async_environment")
def test_wsgi_upload():
client = httpx.Client(app=echo_body)
- response = client.post("http://www.example.org/", data=b"example")
+ response = client.post("http://www.example.org/", content=b"example")
assert response.status_code == 200
assert response.text == "example"
def test_wsgi_upload_with_response_stream():
client = httpx.Client(app=echo_body_with_response_stream)
- response = client.post("http://www.example.org/", data=b"example")
+ response = client.post("http://www.example.org/", content=b"example")
assert response.status_code == 200
assert response.text == "example"
path = raw_path.decode("ascii")
request_headers = httpx.Headers(headers)
- data = (
+ content = (
(item for item in stream)
if stream
and (
method=method.decode("ascii"),
url=f"{scheme}://{host}{port_str}{path}",
headers=request_headers,
- data=data,
+ content=content,
)
request.read()
response = self.handler(request)