From bfe041a0db08c2cd8d12d0dd2f3675459943aceb Mon Sep 17 00:00:00 2001 From: Chau Vu / CPF-FAMILY Date: Mon, 5 Oct 2026 17:24:15 +0700 Subject: [PATCH 1/2] fix(bedrock): guard _chunk_bytes_to_sse against None and non-bytes payload --- src/anthropic/lib/bedrock/_stream_decoder.py | 7 +++++-- tests/lib/test_bedrock.py | 5 +++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/anthropic/lib/bedrock/_stream_decoder.py b/src/anthropic/lib/bedrock/_stream_decoder.py index f77c2a329..3818c9344 100644 --- a/src/anthropic/lib/bedrock/_stream_decoder.py +++ b/src/anthropic/lib/bedrock/_stream_decoder.py @@ -65,10 +65,13 @@ def _parse_message_from_event(self, event: EventStreamMessage) -> ServerSentEven if not chunk: return None - return _chunk_bytes_to_sse(chunk.get("bytes")) + return _chunk_bytes_to_sse(chunk.get("bytes")) # pyright: ignore[reportArgumentType] -def _chunk_bytes_to_sse(raw: bytes) -> ServerSentEvent | None: +def _chunk_bytes_to_sse(raw: bytes | None) -> ServerSentEvent | None: + if raw is None or not isinstance(raw, (bytes, bytearray)): # pyright: ignore[reportUnnecessaryIsInstance] + return None + decoded = raw.decode() data: Any try: diff --git a/tests/lib/test_bedrock.py b/tests/lib/test_bedrock.py index bc97af03f..b4c642caf 100644 --- a/tests/lib/test_bedrock.py +++ b/tests/lib/test_bedrock.py @@ -348,6 +348,11 @@ def test_chunk_bytes_to_sse_drops_chunk_without_type_or_completion() -> None: assert _chunk_bytes_to_sse(raw) is None +def test_chunk_bytes_to_sse_drops_none_and_non_bytes() -> None: + assert _chunk_bytes_to_sse(None) is None + assert _chunk_bytes_to_sse(t.cast(t.Any, 123)) is None + + def _eventstream_chunk_frame(payload: t.Mapping[str, object]) -> bytes: """Encode `payload` as one `chunk` event in AWS eventstream binary framing.""" body = json.dumps({"bytes": base64.b64encode(json.dumps(payload).encode()).decode()}).encode() From 02aa9a197ab1a474bdac27d14f3a0b92022ea1df Mon Sep 17 00:00:00 2001 From: Chau Vu / CPF-FAMILY Date: Fri, 16 Oct 2026 21:59:48 +0700 Subject: [PATCH 2/2] fix(bedrock): guard call-site against empty or missing bytes in stream decoder - Add falsy guard at chunk bytes extraction site to drop None and empty bytes - Restore clean bytes signature for _chunk_bytes_to_sse without pyright ignores - Add regression test for empty and null bytes frames in bedrock eventstream --- src/anthropic/lib/bedrock/_stream_decoder.py | 9 +++--- tests/lib/test_bedrock.py | 32 +++++++++++++++++--- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/anthropic/lib/bedrock/_stream_decoder.py b/src/anthropic/lib/bedrock/_stream_decoder.py index 3818c9344..3ceb169bf 100644 --- a/src/anthropic/lib/bedrock/_stream_decoder.py +++ b/src/anthropic/lib/bedrock/_stream_decoder.py @@ -65,13 +65,14 @@ def _parse_message_from_event(self, event: EventStreamMessage) -> ServerSentEven if not chunk: return None - return _chunk_bytes_to_sse(chunk.get("bytes")) # pyright: ignore[reportArgumentType] + raw = chunk.get("bytes") + if not raw: + return None + return _chunk_bytes_to_sse(raw) -def _chunk_bytes_to_sse(raw: bytes | None) -> ServerSentEvent | None: - if raw is None or not isinstance(raw, (bytes, bytearray)): # pyright: ignore[reportUnnecessaryIsInstance] - return None +def _chunk_bytes_to_sse(raw: bytes) -> ServerSentEvent | None: decoded = raw.decode() data: Any try: diff --git a/tests/lib/test_bedrock.py b/tests/lib/test_bedrock.py index b4c642caf..868a12ff9 100644 --- a/tests/lib/test_bedrock.py +++ b/tests/lib/test_bedrock.py @@ -348,11 +348,6 @@ def test_chunk_bytes_to_sse_drops_chunk_without_type_or_completion() -> None: assert _chunk_bytes_to_sse(raw) is None -def test_chunk_bytes_to_sse_drops_none_and_non_bytes() -> None: - assert _chunk_bytes_to_sse(None) is None - assert _chunk_bytes_to_sse(t.cast(t.Any, 123)) is None - - def _eventstream_chunk_frame(payload: t.Mapping[str, object]) -> bytes: """Encode `payload` as one `chunk` event in AWS eventstream binary framing.""" body = json.dumps({"bytes": base64.b64encode(json.dumps(payload).encode()).decode()}).encode() @@ -429,6 +424,33 @@ async def test_stream_skips_typeless_chunk_async(respx_mock: MockRouter) -> None assert events[1].to_dict()["amazon-bedrock-invocationMetrics"] == _INVOCATION_METRICS +@pytest.mark.respx() +def test_stream_skips_empty_and_null_bytes_chunk(respx_mock: MockRouter) -> None: + body = ( + _eventstream_chunk_frame(_TYPELESS_CHUNK_STREAM[0]) + + _eventstream_frame( + {":message-type": "event", ":event-type": "chunk", ":content-type": "application/json"}, + b'{"bytes": ""}', + ) + + _eventstream_frame( + {":message-type": "event", ":event-type": "chunk", ":content-type": "application/json"}, + b'{"bytes": null}', + ) + + _eventstream_chunk_frame(_TYPELESS_CHUNK_STREAM[2]) + ) + respx_mock.post(_STREAM_URL).mock( + return_value=httpx2.Response( + 200, content=body, headers={"content-type": "application/vnd.amazon.eventstream"} + ) + ) + stream = sync_client.messages.create( + max_tokens=8, messages=[{"role": "user", "content": "hi"}], model="anthropic.claude-x", stream=True + ) + events = list(stream) + + assert [e.type for e in events] == ["message_start", "message_stop"] + + _THROTTLING_MESSAGE = "Too many requests, please wait before trying again." _EXCEPTION_FRAME = _eventstream_frame( {":message-type": "exception", ":exception-type": "throttlingException", ":content-type": "application/json"},