From 3e5c76eb0a6bbfe9575e29e298b26d6239bf3c5d Mon Sep 17 00:00:00 2001 From: Yufeng He <40085740+he-yufeng@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:23:07 +0800 Subject: [PATCH] fix(serialization): strip base64 image data from streamed values events (#3631) The SSE stream's `values` snapshots serialize the full state, including the hide_from_ui human messages that ViewImageMiddleware fills with base64 image payloads. #3535 stripped those from the REST wait/history/state endpoints via serialize_channel_values_for_api, but the streaming path (worker publishes serialize(chunk, mode="values")) still went through serialize_channel_values, so the same base64 leaked to the frontend over SSE. Route values-mode serialization through serialize_channel_values_for_api as well. Non-hidden messages and https image URLs are left untouched. --- .../harness/deerflow/runtime/serialization.py | 7 +++-- backend/tests/test_serialization.py | 29 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/backend/packages/harness/deerflow/runtime/serialization.py b/backend/packages/harness/deerflow/runtime/serialization.py index 9f6c105b3..a9b5cea2f 100644 --- a/backend/packages/harness/deerflow/runtime/serialization.py +++ b/backend/packages/harness/deerflow/runtime/serialization.py @@ -133,11 +133,14 @@ def serialize(obj: Any, *, mode: str = "") -> Any: """Serialize LangChain objects with mode-specific handling. * ``messages`` — obj is ``(message_chunk, metadata_dict)`` - * ``values`` — obj is the full state dict; ``__pregel_*`` keys stripped + * ``values`` — obj is the full state dict; ``__pregel_*`` keys stripped and + base64 ``data:`` image blocks dropped from hide_from_ui messages * everything else — recursive ``model_dump()`` / ``dict()`` fallback """ if mode == "messages": return serialize_messages_tuple(obj) if mode == "values": - return serialize_channel_values(obj) if isinstance(obj, dict) else serialize_lc_object(obj) + # ``values`` snapshots stream the full state to the frontend, so they + # must drop base64 image payloads the same way the REST endpoints do. + return serialize_channel_values_for_api(obj) if isinstance(obj, dict) else serialize_lc_object(obj) return serialize_lc_object(obj) diff --git a/backend/tests/test_serialization.py b/backend/tests/test_serialization.py index be57f9911..f62f5b8af 100644 --- a/backend/tests/test_serialization.py +++ b/backend/tests/test_serialization.py @@ -328,3 +328,32 @@ def test_serialize_channel_values_for_api_no_messages(): result = serialize_channel_values_for_api({"title": "empty"}) assert result == {"title": "empty"} + + +def test_serialize_values_mode_strips_base64_from_hidden_messages(): + """The SSE stream emits ``values`` snapshots of the full state, so it must + strip base64 image data from hide_from_ui messages just like the REST + endpoints do — otherwise the same payload leaks over the stream.""" + import json + + from deerflow.runtime.serialization import serialize + + state = { + "messages": [ + _make_msg( + [ + {"type": "text", "text": "context"}, + { + "type": "image_url", + "image_url": {"url": "data:image/png;base64,iVBOR..."}, + }, + ], + hide_from_ui=True, + ), + ], + } + result = serialize(state, mode="values") + # the hidden message survives (count/order preserved) but the data: block is gone + assert len(result["messages"]) == 1 + assert "data:image/png;base64" not in json.dumps(result) + assert result["messages"][0]["content"] == [{"type": "text", "text": "context"}]