diff --git a/sdk/go/agent/control_plane_memory_backend.go b/sdk/go/agent/control_plane_memory_backend.go index 776e3d72..4cc20e61 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 } @@ -81,11 +85,7 @@ func (b *ControlPlaneMemoryBackend) Get(scope MemoryScope, scopeID, key string) return nil, false, err } - body := map[string]any{ - "key": key, - "scope": b.apiScope(scope), - } - req, err := http.NewRequest(http.MethodPost, endpoint, mustJSONReader(body)) + req, err := http.NewRequest(http.MethodPost, endpoint, keyScopeReader(key, b.apiScope(scope))) if err != nil { return nil, false, err } @@ -118,11 +118,7 @@ func (b *ControlPlaneMemoryBackend) Delete(scope MemoryScope, scopeID, key strin return err } - body := map[string]any{ - "key": key, - "scope": b.apiScope(scope), - } - req, err := http.NewRequest(http.MethodPost, endpoint, mustJSONReader(body)) + req, err := http.NewRequest(http.MethodPost, endpoint, keyScopeReader(key, b.apiScope(scope))) if err != nil { return err } @@ -200,7 +196,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 +285,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 +403,22 @@ 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 +} + +func keyScopeReader(key, scope string) io.Reader { + var body bytes.Buffer + _ = json.NewEncoder(&body).Encode(struct { + Key string `json:"key"` + Scope string `json:"scope"` + }{ + Key: key, + Scope: scope, + }) + return &body } diff --git a/sdk/go/agent/control_plane_memory_backend_test.go b/sdk/go/agent/control_plane_memory_backend_test.go index c2d61766..1fa4ee82 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) + }) }