Skip to content

fix(http): serialize object and boolean query parameters to prevent TypeError - #122

Open
Sooriya04 wants to merge 6 commits into
universal-tool-calling-protocol:mainfrom
Sooriya04:fix/http-object-query-params
Open

Sooriya04 wants to merge 6 commits into
universal-tool-calling-protocol:mainfrom
Sooriya04:fix/http-object-query-params

Conversation

@Sooriya04

@Sooriya04 Sooriya04 commented Nov 4, 2026 •

Copy link
Copy Markdown

Fixes #107.

Problem

When calling call_tool with object/dict query arguments (e.g. content-form parameters like tool_args={"filter": {"status": "open"}}), remaining_args was passed directly into aiohttp's params=. Since aiohttp/yarl only accepts primitives or lists for repeated parameters, any object or boolean raised an unhandled TypeError before sending the HTTP request.

Solution

  • Added _encode_query_params helper across HttpCommunicationProtocol, SseCommunicationProtocol, and StreamableHttpCommunicationProtocol:
    • Dicts and sets are JSON-encoded (using compact separators=(",", ":")).
    • Booleans are converted to 'true' / 'false'.
    • Sequences (lists, tuples) are recursively encoded so aiohttp preserves repeated query parameters (?k=v1&k=v2).
    • Primitives (str, int, float) are preserved as-is.
  • Added regression unit test in plugins/communication_protocols/http/tests/test_http_communication_protocol.py to verify serialization of objects, lists, booleans, and integers.

Testing

  • Ran pytest plugins/communication_protocols/http/tests/ (310 passed, 0 failures).

Summary by cubic

Fixes #107. Calling call_tool with object or boolean query arguments raised an unhandled TypeError because aiohttp only accepts primitives or lists for query params. Query values are now serialized with a shared encode_query_params helper before being passed to aiohttp:

  • Dicts and sets are JSON-encoded with compact separators; sets are sorted for deterministic output, nested sets become sorted lists, non-string dict keys are normalized to strings, and non-JSON values fall back to str().
  • Booleans become 'true'/'false'; lists and tuples are recursively encoded so repeated parameters are preserved (?tag=a&tag=b); primitives pass through unchanged, and None values are dropped.
  • Raises a descriptive ValueError when distinct dictionary keys normalize to the same string (e.g. 100 and '100') instead of silently overwriting.
  • Applied to HttpCommunicationProtocol, SseCommunicationProtocol, and StreamableHttpCommunicationProtocol, with regression tests added for objects, sets, nested sequences, booleans, None dropping, non-string keys, and key collisions.

Written for commit 95fbe9a. Summary will update on new commits.

View guided diff

…ypeError

Fixes universal-tool-calling-protocol#107.

- JSON-encodes object/dictionary and set query parameters so nested content-form
  query parameters do not raise unhandled TypeError in aiohttp.
- Normalizes boolean parameters to 'true'/'false'.
- Preserves sequence/list repetition (?tag=a&tag=b) and primitives.
- Applied uniformly across HttpCommunicationProtocol, SseCommunicationProtocol,
  and StreamableHttpCommunicationProtocol.
- Adds regression unit test verifying object and list query parameter serialization.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="plugins/communication_protocols/http/src/utcp_http/http_communication_protocol.py">

<violation number="1" location="plugins/communication_protocols/http/src/utcp_http/http_communication_protocol.py:503">
P2: Set-valued query arguments still raise `TypeError` because `json.dumps` cannot encode a set. Convert sets to JSON arrays in all three protocol encoders.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

Comment on lines +503 to +504
if isinstance(val, (dict, set)):
return json.dumps(val, separators=(",", ":"))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Set-valued query arguments still raise TypeError because json.dumps cannot encode a set. Convert sets to JSON arrays in all three protocol encoders.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At plugins/communication_protocols/http/src/utcp_http/http_communication_protocol.py, line 503:

<comment>Set-valued query arguments still raise `TypeError` because `json.dumps` cannot encode a set. Convert sets to JSON arrays in all three protocol encoders.</comment>

<file context>
@@ -482,6 +482,30 @@ async def _handle_oauth2(self, auth_details: OAuth2Auth) -> str:
+                return val
+            if isinstance(val, (list, tuple)):
+                return [_encode_val(item) for item in val]
+            if isinstance(val, (dict, set)):
+                return json.dumps(val, separators=(",", ":"))
+            return str(val)
</file context>
Suggested change
if isinstance(val, (dict, set)):
return json.dumps(val, separators=(",", ":"))
if isinstance(val, dict):
return json.dumps(val, separators=(",", ":"))
if isinstance(val, set):
return json.dumps(list(val), separators=(",", ":"))

…nested sequences

Addresses cubic-dev-ai review feedback on PR universal-tool-calling-protocol#122:
- Consolidates _encode_query_params into shared encode_query_params in _security.py.
- Converts set values into sorted JSON arrays to avoid TypeError from json.dumps.
- JSON-encodes nested sequences/lists so aiohttp does not raise TypeError on matrix/nested arguments.
- Adds test cases for set and nested sequence arguments to test_http_communication_protocol.py.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 existing issue remains and 1 new issue found across 5 files (changes from recent commits).

Requires human review: Auto-approval blocked because this review re-detected 1 unresolved issue already reported by Cubic.

View guided diff | Re-trigger cubic

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="plugins/communication_protocols/http/src/utcp_http/_security.py">

<violation number="1" location="plugins/communication_protocols/http/src/utcp_http/_security.py:667">
P2: A `None` argument is encoded as an empty string, so an optional argument passed as null is sent as `key=`. Many servers treat that differently from an absent parameter. Drop `None` values from the query instead, or document the mapping.</violation>
</file>

- Drops query parameters with None values so optional unset arguments
  are not sent as 'key=' on the wire.
- Adds test assertion verifying 'optional_unset' is omitted from the request query.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

…query arguments

- Adds a default handler to json.dumps in encode_query_params:
  - Nested sets/frozensets serialize to sorted lists.
  - Other non-JSON types fall back to str(obj).
- Updates regression test in test_http_communication_protocol.py with nested sets.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

…query parameters

- Normalizes nested dictionaries so non-string keys (e.g. ints or tuples)
  become strings before json.dumps, preventing TypeError since json.dumps
  default handler only operates on values.
- Recursively sorts sets into lists and preserves nested structures.
- Adds regression test with numeric dictionary key {100: 'numeric_key'}.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 2 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="plugins/communication_protocols/http/src/utcp_http/_security.py">

<violation number="1" location="plugins/communication_protocols/http/src/utcp_http/_security.py:670">
P2: This normalization silently overwrites distinct keys such as `100` and `'100'`, dropping one field from the JSON query value. Detect collisions and reject or encode them without loss.</violation>
</file>

Reply with feedback, questions, or to request a fix.

View guided diff | Re-trigger cubic

"""
def _normalize(obj: Any) -> Any:
if isinstance(obj, dict):
return {str(k): _normalize(v) for k, v in obj.items()}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This normalization silently overwrites distinct keys such as 100 and '100', dropping one field from the JSON query value. Detect collisions and reject or encode them without loss.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. At plugins/communication_protocols/http/src/utcp_http/_security.py, line 670:

<comment>This normalization silently overwrites distinct keys such as `100` and `'100'`, dropping one field from the JSON query value. Detect collisions and reject or encode them without loss.</comment>

<file context>
@@ -658,17 +658,21 @@ def encode_query_params(params: Dict[str, Any]) -> Dict[str, Any]:
-    def _json_default(obj: Any) -> Any:
+    def _normalize(obj: Any) -> Any:
+        if isinstance(obj, dict):
+            return {str(k): _normalize(v) for k, v in obj.items()}
         if isinstance(obj, (set, frozenset)):
-            return sorted(obj, key=str)
</file context>
Suggested change
return {str(k): _normalize(v) for k, v in obj.items()}
normalized = {}
for k, v in obj.items():
key = str(k)
if key in normalized:
raise ValueError(f"Dictionary keys collide after string conversion: {key!r}")
normalized[key] = _normalize(v)
return normalized

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 95fbe9a - added collision detection with a ValueError and regression unit test.

…aram normalization

- Detects when two distinct dictionary keys map to the same string representation
  (e.g. 100 and '100') and raises a descriptive ValueError rather than silently
  overwriting and losing data.
- Adds regression unit test verifying collision detection.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

call_tool passes object-valued query params straight to aiohttp, raising an unhandled TypeError instead of JSON-encoding them

1 participant