From 1612c9ab2f8b0faf4f3c85ccb2f3e2b2a0f50efc Mon Sep 17 00:00:00 2001 From: Ardit Tirana Date: Sun, 21 Jun 2026 23:38:06 +0200 Subject: [PATCH] fix(go-sdk): surface json.Marshal errors in memory backend requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mustJSONReader discarded the json.Marshal error and returned a reader over a nil byte slice. When a value could not be serialized (unsupported type, cyclic struct, channel, func), the control-plane POST was sent with an empty body — silently storing nothing or overwriting with an empty value — and the real error never reached the caller. Replace it with jsonReader(v any) (io.Reader, error) and propagate the error at each call site (Set, Get, Delete, SetVector, SearchVector), all of which already return an error. Adds a test asserting an unserializable value yields an error instead of an empty reader. Fixes #434. --- sdk/go/agent/control_plane_memory_backend.go | 39 +++++++++++++++---- .../control_plane_memory_backend_test.go | 14 ++++++- 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/sdk/go/agent/control_plane_memory_backend.go b/sdk/go/agent/control_plane_memory_backend.go index 776e3d723..0cd4522dd 100644 --- a/sdk/go/agent/control_plane_memory_backend.go +++ b/sdk/go/agent/control_plane_memory_backend.go @@ -56,7 +56,11 @@ func (b *ControlPlaneMemoryBackend) Set(scope MemoryScope, scopeID, key string, "data": value, "scope": b.apiScope(scope), } - req, err := http.NewRequest(http.MethodPost, endpoint, mustJSONReader(body)) + reader, err := jsonReader(body) + if err != nil { + return err + } + req, err := http.NewRequest(http.MethodPost, endpoint, reader) if err != nil { return err } @@ -85,7 +89,11 @@ func (b *ControlPlaneMemoryBackend) Get(scope MemoryScope, scopeID, key string) "key": key, "scope": b.apiScope(scope), } - req, err := http.NewRequest(http.MethodPost, endpoint, mustJSONReader(body)) + reader, err := jsonReader(body) + if err != nil { + return nil, false, err + } + req, err := http.NewRequest(http.MethodPost, endpoint, reader) if err != nil { return nil, false, err } @@ -122,7 +130,11 @@ func (b *ControlPlaneMemoryBackend) Delete(scope MemoryScope, scopeID, key strin "key": key, "scope": b.apiScope(scope), } - req, err := http.NewRequest(http.MethodPost, endpoint, mustJSONReader(body)) + reader, err := jsonReader(body) + if err != nil { + return err + } + req, err := http.NewRequest(http.MethodPost, endpoint, reader) if err != nil { return err } @@ -200,7 +212,11 @@ func (b *ControlPlaneMemoryBackend) SetVector(scope MemoryScope, scopeID, key st "metadata": metadata, "scope": b.apiScope(scope), } - req, err := http.NewRequest(http.MethodPost, endpoint, mustJSONReader(body)) + reader, err := jsonReader(body) + if err != nil { + return err + } + req, err := http.NewRequest(http.MethodPost, endpoint, reader) if err != nil { return err } @@ -285,7 +301,11 @@ func (b *ControlPlaneMemoryBackend) SearchVector(scope MemoryScope, scopeID stri body["scope"] = b.apiScope(opts.Scope) } - req, err := http.NewRequest(http.MethodPost, endpoint, mustJSONReader(body)) + reader, err := jsonReader(body) + if err != nil { + return nil, err + } + req, err := http.NewRequest(http.MethodPost, endpoint, reader) if err != nil { return nil, err } @@ -399,7 +419,10 @@ func (b *ControlPlaneMemoryBackend) apiScope(scope MemoryScope) string { } } -func mustJSONReader(v any) io.Reader { - data, _ := json.Marshal(v) - return bytes.NewReader(data) +func jsonReader(v any) (io.Reader, error) { + data, err := json.Marshal(v) + if err != nil { + return nil, err + } + return bytes.NewReader(data), nil } diff --git a/sdk/go/agent/control_plane_memory_backend_test.go b/sdk/go/agent/control_plane_memory_backend_test.go index c2d61766e..1fa4ee824 100644 --- a/sdk/go/agent/control_plane_memory_backend_test.go +++ b/sdk/go/agent/control_plane_memory_backend_test.go @@ -201,7 +201,7 @@ func TestControlPlaneMemoryBackend_ErrorPathsAndHelpers(t *testing.T) { assert.Contains(t, err.Error(), "vector memory search failed") }) - t.Run("scope helpers and mustJSONReader", func(t *testing.T) { + t.Run("scope helpers and jsonReader", func(t *testing.T) { b := NewControlPlaneMemoryBackend("http://example.com///", "token-123", "agent-1") assert.Equal(t, "http://example.com", b.baseURL) assert.Equal(t, "workflow", b.apiScope(ScopeWorkflow)) @@ -210,8 +210,18 @@ func TestControlPlaneMemoryBackend_ErrorPathsAndHelpers(t *testing.T) { assert.Equal(t, "global", b.apiScope(ScopeGlobal)) assert.Equal(t, "global", b.apiScope(MemoryScope("unexpected"))) - body, err := io.ReadAll(mustJSONReader(map[string]any{"ok": true})) + reader, err := jsonReader(map[string]any{"ok": true}) + require.NoError(t, err) + body, err := io.ReadAll(reader) require.NoError(t, err) assert.JSONEq(t, `{"ok":true}`, string(body)) }) + + t.Run("jsonReader surfaces marshal errors", func(t *testing.T) { + // Values that cannot be serialized (e.g. a channel) must return an + // error instead of silently yielding an empty reader. See issue #434. + reader, err := jsonReader(map[string]any{"bad": make(chan int)}) + require.Error(t, err) + assert.Nil(t, reader) + }) }