From da412162157bea0a59fe7c2c697a71124705ceae Mon Sep 17 00:00:00 2001 From: SamRemis Date: Wed, 25 Mar 2026 20:05:47 -0400 Subject: [PATCH] Stop iterating response body on error for bidirectional streams --- ...ugfix-cec2955f4e8e4c1cbd89918bf72de36a.json | 4 ++++ .../smithy-http/src/smithy_http/aio/crt.py | 4 ++++ .../smithy-http/tests/unit/aio/test_crt.py | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 packages/smithy-http/.changes/next-release/smithy-http-bugfix-cec2955f4e8e4c1cbd89918bf72de36a.json diff --git a/packages/smithy-http/.changes/next-release/smithy-http-bugfix-cec2955f4e8e4c1cbd89918bf72de36a.json b/packages/smithy-http/.changes/next-release/smithy-http-bugfix-cec2955f4e8e4c1cbd89918bf72de36a.json new file mode 100644 index 000000000..3fc0f276f --- /dev/null +++ b/packages/smithy-http/.changes/next-release/smithy-http-bugfix-cec2955f4e8e4c1cbd89918bf72de36a.json @@ -0,0 +1,4 @@ +{ + "type": "bugfix", + "description": "Fix infinite hang when a bidirectional stream receives a non-2xx HTTP response from the server" +} diff --git a/packages/smithy-http/src/smithy_http/aio/crt.py b/packages/smithy-http/src/smithy_http/aio/crt.py index 5e5cc861c..165a6f5a0 100644 --- a/packages/smithy-http/src/smithy_http/aio/crt.py +++ b/packages/smithy-http/src/smithy_http/aio/crt.py @@ -102,6 +102,10 @@ async def chunks(self) -> AsyncGenerator[bytes, None]: chunk = await self._stream.get_next_response_chunk() if chunk: yield chunk + # CRT will hang infinitely and not close the stream if it is closed server side; to work around this, + # we need to break this loop ourselves after reading the first chunk. + if self._status >= 400: + break else: break diff --git a/packages/smithy-http/tests/unit/aio/test_crt.py b/packages/smithy-http/tests/unit/aio/test_crt.py index 886bb5f2d..530646713 100644 --- a/packages/smithy-http/tests/unit/aio/test_crt.py +++ b/packages/smithy-http/tests/unit/aio/test_crt.py @@ -392,3 +392,21 @@ def test_response_properties() -> None: assert response.status == 404 assert response.fields == fields assert response.reason is None + + +async def test_error_response_chunks_stops_after_first_chunk() -> None: + """Test that error responses stop reading after the first chunk.""" + mock_stream = AsyncMock() + mock_stream.get_next_response_chunk.side_effect = [ + b"error body", + b"should not be read", + b"", + ] + + response = AWSCRTHTTPResponse(status=500, fields=Fields(), stream=mock_stream) + + chunks: list[bytes] = [] + async for chunk in response.chunks(): + chunks.append(chunk) + + assert chunks == [b"error body"]