Skip to content

Commit 04c6cc1

Browse files
refactor(runtime): Improve JSON serialization for consistent handling (#14)
Refactor JSON serialization to ensure consistent handling between streaming and non-streaming responses. This change: 1. Adds a new _safe_serialize_to_json_string method that handles serialization with progressive fallbacks for non-serializable objects 2. Updates _convert_to_sse to use the new method for consistency 3. Modifies non-streaming responses to use the same serialization logic 4. Adds comprehensive tests for serialization edge cases This ensures that both streaming and non-streaming responses handle complex objects like datetime, Decimal, sets, and Unicode characters consistently. 🤖 Assisted by [Amazon Q Developer](https://aws.amazon.com/q/developer) cr: https://code.amazon.com/reviews/CR-209344425
1 parent 0cf5608 commit 04c6cc1

File tree

2 files changed

+542
-14
lines changed

2 files changed

+542
-14
lines changed

src/bedrock_agentcore/runtime/app.py

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from typing import Any, Callable, Dict, Optional
1616

1717
from starlette.applications import Starlette
18-
from starlette.responses import JSONResponse, StreamingResponse
18+
from starlette.responses import JSONResponse, Response, StreamingResponse
1919
from starlette.routing import Route
2020

2121
from .context import BedrockAgentCoreContext, RequestContext
@@ -305,7 +305,9 @@ async def _handle_invocation(self, request):
305305
return StreamingResponse(self._stream_with_error_handling(result), media_type="text/event-stream")
306306

307307
self.logger.info("Invocation completed successfully (%.3fs)", duration)
308-
return JSONResponse(result)
308+
# Use safe serialization for consistency with streaming paths
309+
safe_json_string = self._safe_serialize_to_json_string(result)
310+
return Response(safe_json_string, media_type="application/json")
309311

310312
except json.JSONDecodeError as e:
311313
duration = time.time() - start_time
@@ -406,18 +408,6 @@ def _handle_task_action(self, payload: dict) -> Optional[JSONResponse]:
406408
self.logger.error("Debug action '%s' failed: %s: %s", action, type(e).__name__, e)
407409
return JSONResponse({"error": "Debug action failed", "details": str(e)}, status_code=500)
408410

409-
def _convert_to_sse(self, chunk):
410-
try:
411-
return f"data: {json.dumps(chunk)}\n\n".encode("utf-8")
412-
except (TypeError, ValueError):
413-
try:
414-
return f"data: {json.dumps(str(chunk))}\n\n".encode("utf-8")
415-
except (TypeError, ValueError) as e:
416-
self.logger.warning("Failed to serialize SSE chunk: %s: %s", type(e).__name__, e)
417-
error_data = {"error": "Serialization failed", "original_type": type(chunk).__name__}
418-
sse_string = f"data: {json.dumps(error_data)}\n\n"
419-
return sse_string.encode("utf-8")
420-
421411
async def _stream_with_error_handling(self, generator):
422412
"""Wrap async generator to handle errors and convert to SSE format."""
423413
try:
@@ -432,6 +422,42 @@ async def _stream_with_error_handling(self, generator):
432422
}
433423
yield self._convert_to_sse(error_event)
434424

425+
def _safe_serialize_to_json_string(self, obj):
426+
"""Safely serialize object directly to JSON string with progressive fallback handling.
427+
428+
This method eliminates double JSON encoding by returning the JSON string directly,
429+
avoiding the test-then-encode pattern that leads to redundant json.dumps() calls.
430+
Used by both streaming and non-streaming responses for consistent behavior.
431+
432+
Returns:
433+
str: JSON string representation of the object
434+
"""
435+
try:
436+
# First attempt: direct JSON serialization with Unicode support
437+
return json.dumps(obj, ensure_ascii=False)
438+
except (TypeError, ValueError, UnicodeEncodeError):
439+
try:
440+
# Second attempt: convert to string, then JSON encode the string
441+
return json.dumps(str(obj), ensure_ascii=False)
442+
except Exception as e:
443+
# Final fallback: JSON encode error object with ASCII fallback for problematic Unicode
444+
self.logger.warning("Failed to serialize object: %s: %s", type(e).__name__, e)
445+
error_obj = {"error": "Serialization failed", "original_type": type(obj).__name__}
446+
return json.dumps(error_obj, ensure_ascii=False)
447+
448+
def _convert_to_sse(self, obj) -> bytes:
449+
"""Convert object to Server-Sent Events format using safe serialization.
450+
451+
Args:
452+
obj: Object to convert to SSE format
453+
454+
Returns:
455+
bytes: SSE-formatted data ready for streaming
456+
"""
457+
json_string = self._safe_serialize_to_json_string(obj)
458+
sse_data = f"data: {json_string}\n\n"
459+
return sse_data.encode("utf-8")
460+
435461
def _sync_stream_with_error_handling(self, generator):
436462
"""Wrap sync generator to handle errors and convert to SSE format."""
437463
try:

0 commit comments

Comments
 (0)