From 2dff5e0094190b6b3c42e9d3274f277dd3134bf4 Mon Sep 17 00:00:00 2001 From: Joey Ekstrom Date: Mon, 23 Jun 2025 16:04:41 -0700 Subject: [PATCH 1/4] Updated _content.py: encode_content to accept a bytearray as well as bytes. This allows sending a mutable bytearray without having to cast to an immutable bytes object, trigging a memory copy before sending. --- httpx/_content.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/httpx/_content.py b/httpx/_content.py index 6f479a0885..2d2421577b 100644 --- a/httpx/_content.py +++ b/httpx/_content.py @@ -105,9 +105,9 @@ async def __aiter__(self) -> AsyncIterator[bytes]: def encode_content( - content: str | bytes | Iterable[bytes] | AsyncIterable[bytes], + content: str | bytes | bytearray | Iterable[bytes] | AsyncIterable[bytes], ) -> tuple[dict[str, str], SyncByteStream | AsyncByteStream]: - if isinstance(content, (bytes, str)): + if isinstance(content, (bytes, bytearray, str)): body = content.encode("utf-8") if isinstance(content, str) else content content_length = len(body) headers = {"Content-Length": str(content_length)} if body else {} From 7b829bc9bf16d9b4d29dc870814328bf7b7cd880 Mon Sep 17 00:00:00 2001 From: Joey Ekstrom Date: Tue, 24 Jun 2025 08:56:19 -0700 Subject: [PATCH 2/4] Added test for bytearray content being passed into "encode_content()" --- test | 1 + tests/test_content.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 test diff --git a/test b/test new file mode 100644 index 0000000000..a7c01bc6a4 --- /dev/null +++ b/test @@ -0,0 +1 @@ +# TLS secrets log file, generated by OpenSSL / Python diff --git a/tests/test_content.py b/tests/test_content.py index f63ec18a6b..e987104a9a 100644 --- a/tests/test_content.py +++ b/tests/test_content.py @@ -50,6 +50,30 @@ async def test_bytes_content(): assert async_content == b"Hello, world!" +@pytest.mark.anyio +async def test_bytearray_content(): + request = httpx.Request(method, url, content=b"Hello, world!") + assert isinstance(request.stream, typing.Iterable) + assert isinstance(request.stream, typing.AsyncIterable) + + sync_content = b"".join(list(request.stream)) + async_content = b"".join([part async for part in request.stream]) + + assert request.headers == {"Host": "www.example.com", "Content-Length": "13"} + assert sync_content == b"Hello, world!" + assert async_content == b"Hello, world!" + + assert isinstance(request.stream, typing.Iterable) + assert isinstance(request.stream, typing.AsyncIterable) + + sync_content = b"".join(list(request.stream)) + async_content = b"".join([part async for part in request.stream]) + + assert request.headers == {"Host": "www.example.com", "Content-Length": "13"} + assert sync_content == b"Hello, world!" + assert async_content == b"Hello, world!" + + @pytest.mark.anyio async def test_bytesio_content(): request = httpx.Request(method, url, content=io.BytesIO(b"Hello, world!")) From 64dc8f24e99604220d618132bd5fd57db4524cb5 Mon Sep 17 00:00:00 2001 From: Joey Ekstrom Date: Tue, 24 Jun 2025 09:03:55 -0700 Subject: [PATCH 3/4] Updated RequestContent to include bytearray in the Union --- httpx/_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/httpx/_types.py b/httpx/_types.py index 704dfdffc8..76fa3057ce 100644 --- a/httpx/_types.py +++ b/httpx/_types.py @@ -65,7 +65,7 @@ "Auth", ] -RequestContent = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]] +RequestContent = Union[str, bytes, bytearray, Iterable[bytes], AsyncIterable[bytes]] ResponseContent = Union[str, bytes, Iterable[bytes], AsyncIterable[bytes]] ResponseExtensions = Mapping[str, Any] From d5c6356b8703dd61abd457e363409543cd85e852 Mon Sep 17 00:00:00 2001 From: Joey Ekstrom Date: Tue, 24 Jun 2025 09:08:14 -0700 Subject: [PATCH 4/4] Updated docs to explicityly state bytearray is allowed. --- docs/api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.md b/docs/api.md index f1bd50c993..5b189eb06e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -100,7 +100,7 @@ what gets sent over the wire.* * `def __init__(method, url, [params], [headers], [cookies], [content], [data], [files], [json], [stream])` * `.method` - **str** * `.url` - **URL** -* `.content` - **byte**, **byte iterator**, or **byte async iterator** +* `.content` - **byte**, **bytearray**, **byte iterator**, or **byte async iterator** * `.headers` - **Headers** * `.cookies` - **Cookies**