Skip to content

Commit 3b35a4f

Browse files
committed
smithy-http: Fix AWSCRTHTTPClient request bodies for HTTP/1.1 connections
1 parent 720c0cb commit 3b35a4f

3 files changed

Lines changed: 137 additions & 7 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"type": "bugfix",
3+
"description": "Fixed `AWSCRTHTTPClient` to send HTTP/1.1 request bodies via `body_stream` instead of the HTTP/2-only `request_body_generator`."
4+
}

packages/smithy-http/src/smithy_http/aio/crt.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from copy import deepcopy
77
from dataclasses import dataclass
88
from inspect import iscoroutinefunction
9+
from io import BytesIO
910
from typing import TYPE_CHECKING, Any
1011

1112
from awscrt.exceptions import AwsCrtError
@@ -36,6 +37,7 @@
3637
from smithy_core import interfaces as core_interfaces
3738
from smithy_core.aio import interfaces as core_aio_interfaces
3839
from smithy_core.aio.types import AsyncBytesReader
40+
from smithy_core.aio.utils import read_streaming_blob_async
3941
from smithy_core.exceptions import MissingDependencyError
4042

4143
from .. import Field, Fields
@@ -175,13 +177,18 @@ async def send(
175177
crt_request = self._marshal_request(request)
176178
connection = await self._get_connection(request.destination)
177179

178-
# Convert body to async iterator for request_body_generator
179-
body_generator = self._create_body_generator(request.body)
180-
181-
crt_stream = connection.request(
182-
crt_request,
183-
request_body_generator=body_generator,
184-
)
180+
# request_body_generator is HTTP/2-only in CRT; HTTP/1.1 must use body_stream
181+
if connection.version == crt_http.HttpVersion.Http2:
182+
crt_stream = connection.request(
183+
crt_request,
184+
request_body_generator=self._create_body_generator(request.body),
185+
)
186+
else:
187+
if (
188+
body_stream := await self._create_body_stream(request.body)
189+
) is not None:
190+
crt_request.body_stream = body_stream
191+
crt_stream = connection.request(crt_request)
185192

186193
return await self._await_response(crt_stream)
187194
except AwsCrtError as e:
@@ -308,6 +315,15 @@ def _marshal_request(
308315
)
309316
return crt_request
310317

318+
async def _create_body_stream(
319+
self, body: core_aio_interfaces.StreamingBlob
320+
) -> core_interfaces.BytesReader | None:
321+
"""Convert various body types to a bytes reader for CRT HTTP/1.1."""
322+
if core_interfaces.is_bytes_reader(body):
323+
return body
324+
buffered = await read_streaming_blob_async(body)
325+
return BytesIO(buffered) if buffered else None
326+
311327
async def _create_body_generator(
312328
self, body: core_aio_interfaces.StreamingBlob
313329
) -> AsyncGenerator[bytes, None]:

packages/smithy-http/tests/unit/aio/test_crt.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,69 @@ async def test_port_included_in_host_header(host: str, expected: str) -> None:
6666
assert crt_request.headers.get("host") == expected # type: ignore
6767

6868

69+
async def test_send_http1_uses_body_stream() -> None:
70+
"""Test HTTP/1.1 requests use the CRT body stream."""
71+
client = AWSCRTHTTPClient()
72+
request = HTTPRequest(
73+
method="POST",
74+
destination=URI(scheme="https", host="example.com", path="/"),
75+
body=AsyncBytesReader(b"Action=Test&Version=2020-01-08"),
76+
fields=Fields(),
77+
)
78+
mock_stream = Mock()
79+
mock_response = Mock()
80+
mock_connection = Mock()
81+
mock_connection.version = crt_http.HttpVersion.Http1_1
82+
mock_connection.request = Mock(return_value=mock_stream)
83+
84+
with (
85+
patch.object(
86+
client, "_get_connection", AsyncMock(return_value=mock_connection)
87+
),
88+
patch.object(client, "_await_response", AsyncMock(return_value=mock_response)),
89+
):
90+
actual = await client.send(request)
91+
92+
assert actual is mock_response
93+
mock_connection.request.assert_called_once()
94+
assert "request_body_generator" not in mock_connection.request.call_args.kwargs
95+
crt_request = mock_connection.request.call_args.args[0]
96+
assert crt_request.body_stream is not None
97+
98+
99+
async def test_send_http2_uses_body_generator() -> None:
100+
"""Test HTTP/2 requests use the CRT body generator."""
101+
client = AWSCRTHTTPClient()
102+
request = HTTPRequest(
103+
method="POST",
104+
destination=URI(scheme="https", host="example.com", path="/"),
105+
body=AsyncBytesReader(b"Action=Test&Version=2020-01-08"),
106+
fields=Fields(),
107+
)
108+
mock_stream = Mock()
109+
mock_response = Mock()
110+
mock_connection = Mock()
111+
mock_connection.version = crt_http.HttpVersion.Http2
112+
mock_connection.request = Mock(return_value=mock_stream)
113+
114+
with (
115+
patch.object(
116+
client, "_get_connection", AsyncMock(return_value=mock_connection)
117+
),
118+
patch.object(client, "_await_response", AsyncMock(return_value=mock_response)),
119+
):
120+
actual = await client.send(request)
121+
122+
assert actual is mock_response
123+
mock_connection.request.assert_called_once()
124+
crt_request = mock_connection.request.call_args.args[0]
125+
body_generator = mock_connection.request.call_args.kwargs["request_body_generator"]
126+
assert crt_request.body_stream is None
127+
assert [chunk async for chunk in body_generator] == [
128+
b"Action=Test&Version=2020-01-08"
129+
]
130+
131+
69132
async def test_body_generator_bytes() -> None:
70133
"""Test body generator with bytes input."""
71134
client = AWSCRTHTTPClient()
@@ -191,6 +254,53 @@ async def test_body_generator_empty_bytes() -> None:
191254
assert chunks == [b""]
192255

193256

257+
async def test_body_stream_bytes() -> None:
258+
"""Test body stream with bytes input."""
259+
client = AWSCRTHTTPClient()
260+
261+
body_stream = await client._create_body_stream(b"Hello, World!")
262+
263+
assert body_stream is not None
264+
assert body_stream.read() == b"Hello, World!"
265+
266+
267+
async def test_body_stream_bytesio() -> None:
268+
"""Test body stream with BytesIO."""
269+
client = AWSCRTHTTPClient()
270+
body = BytesIO(b"data from BytesIO")
271+
272+
body_stream = await client._create_body_stream(body)
273+
274+
assert body_stream is not None
275+
assert body_stream is body
276+
assert body_stream.read() == b"data from BytesIO"
277+
278+
279+
async def test_body_stream_async_iterable() -> None:
280+
"""Test body stream with custom AsyncIterable."""
281+
282+
async def custom_generator() -> AsyncIterator[bytes]:
283+
yield b"chunk1"
284+
yield b"chunk2"
285+
yield b"chunk3"
286+
287+
client = AWSCRTHTTPClient()
288+
289+
body_stream = await client._create_body_stream(custom_generator())
290+
291+
assert body_stream is not None
292+
assert body_stream.read() == b"chunk1chunk2chunk3"
293+
294+
295+
async def test_body_stream_empty_bytes() -> None:
296+
"""Test body stream with empty bytes."""
297+
client = AWSCRTHTTPClient()
298+
299+
body_stream = await client._create_body_stream(b"")
300+
301+
assert body_stream is None
302+
303+
194304
async def test_build_connection_http() -> None:
195305
"""Test building HTTP connection."""
196306
client = AWSCRTHTTPClient()

0 commit comments

Comments
 (0)