From 8eaca5b19031893030e5617d910b9e6cf77caee4 Mon Sep 17 00:00:00 2001 From: jariy17 Date: Wed, 23 Sep 2026 21:08:22 +0000 Subject: [PATCH] fix(runtime): send bytes in events as base64 instead of a repr string convert_complex_objects had no case for bytes, so an event holding bytes fell through to json.dumps(str(obj)) and the client got the dict's repr as a JSON string. Encode bytes and bytearray as base64, and convert the output of model_dump() and asdict() too, so bytes inside Pydantic models and dataclasses are covered. Fixes #659 --- src/bedrock_agentcore/runtime/utils.py | 14 +++++++--- tests/bedrock_agentcore/runtime/test_app.py | 8 ++++++ tests/bedrock_agentcore/runtime/test_utils.py | 28 +++++++++++++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/bedrock_agentcore/runtime/utils.py b/src/bedrock_agentcore/runtime/utils.py index 7a074577..9a09fb66 100644 --- a/src/bedrock_agentcore/runtime/utils.py +++ b/src/bedrock_agentcore/runtime/utils.py @@ -1,5 +1,6 @@ """Bedrock AgentCore runtime utilities for object conversion and serialization.""" +import base64 from dataclasses import asdict, is_dataclass from typing import Any @@ -10,13 +11,14 @@ def convert_complex_objects(obj: Any, _depth: int = 0) -> Any: if _depth > 50: return f"" - # Handle Pydantic models (like AIMessage) + # Handle Pydantic models (like AIMessage). The dump can still hold bytes or + # sets, so it is converted too. if hasattr(obj, "model_dump"): - return obj.model_dump() + return convert_complex_objects(obj.model_dump(), _depth + 1) - # Handle dataclasses (like AgentResult) + # Handle dataclasses (like AgentResult), converting the dump for the same reason elif is_dataclass(obj): - return asdict(obj) + return convert_complex_objects(asdict(obj), _depth + 1) # Handle dictionaries recursively elif isinstance(obj, dict): @@ -30,6 +32,10 @@ def convert_complex_objects(obj: Any, _depth: int = 0) -> Any: elif isinstance(obj, set): return [convert_complex_objects(item, _depth + 1) for item in obj] + # Handle binary data (base64, since JSON has no bytes type) + elif isinstance(obj, (bytes, bytearray)): + return base64.b64encode(obj).decode("ascii") + # Return primitives as-is else: return obj diff --git a/tests/bedrock_agentcore/runtime/test_app.py b/tests/bedrock_agentcore/runtime/test_app.py index edc0ff6a..15d64485 100644 --- a/tests/bedrock_agentcore/runtime/test_app.py +++ b/tests/bedrock_agentcore/runtime/test_app.py @@ -1059,6 +1059,14 @@ def test_convert_to_sse_json_serializable_data(self): parsed_data = json.loads(json_part) assert parsed_data == test_data + def test_convert_to_sse_streams_bytes_as_json_object(self): + """An event holding bytes must arrive as a JSON object, not a string of its repr (#659).""" + app = BedrockAgentCoreApp() + + sse_string = app._convert_to_sse({"result": {"redactedContent": b"abc"}}).decode("utf-8") + + assert json.loads(sse_string[6:-2]) == {"result": {"redactedContent": "YWJj"}} + def test_convert_to_sse_non_serializable_object(self): """Test that non-JSON-serializable objects trigger error handling.""" app = BedrockAgentCoreApp() diff --git a/tests/bedrock_agentcore/runtime/test_utils.py b/tests/bedrock_agentcore/runtime/test_utils.py index 5c9ace48..6eccd374 100644 --- a/tests/bedrock_agentcore/runtime/test_utils.py +++ b/tests/bedrock_agentcore/runtime/test_utils.py @@ -1,5 +1,6 @@ """Tests for Bedrock AgentCore runtime utilities.""" +import json from dataclasses import dataclass from typing import List, Optional @@ -178,6 +179,33 @@ def test_sets(self): assert "b" in result assert "c" in result + def test_bytes_become_base64_anywhere_in_the_tree(self): + """Bytes must end up JSON-serializable, including inside dataclasses and Pydantic models.""" + + @dataclass + class Block: + data: bytes + + class Blob(BaseModel): + data: bytes + + event = { + "raw": b"abc", + "buffer": bytearray(b"\x00\xff"), + "dataclass": Block(data=b"abc"), + "pydantic": Blob(data=b"abc"), + } + + result = convert_complex_objects(event) + + assert result == { + "raw": "YWJj", + "buffer": "AP8=", + "dataclass": {"data": "YWJj"}, + "pydantic": {"data": "YWJj"}, + } + json.dumps(result) + def test_nested_sets_with_complex_objects(self): """Test sets containing hashable objects (complex objects can't be in sets)."""