From 9968f9c07f105bae8a6b296aeb6dea873b3b88b0 Mon Sep 17 00:00:00 2001 From: SrzStephen Date: Thu, 19 Feb 2026 03:05:24 +1100 Subject: [PATCH 1/3] fix(deps): `DeprecationWarning` on `HTTP_413_REQUEST_ENTITY_TOO_LARGE` (#693) # Description In [This commit that landed in v0.48.0](https://github.com/Kludex/starlette/commit/40a81479c7146bab21cc58ccb92b017cf177a077) starlette changed constants to conform to [RFC9110](https://www.rfc-editor.org/rfc/rfc9110#name-413-content-too-large). It's the only [one affected](https://github.com/Kludex/starlette/commit/40a81479c7146bab21cc58ccb92b017cf177a077#diff-966c562bff3b11736a75544f42526c942c185b9fe9eb4b7f5a0dabbeca874fa7R180) that I could find in this codebase. - [x] Follow the [`CONTRIBUTING` Guide](https://github.com/a2aproject/a2a-python/blob/main/CONTRIBUTING.md). - [x] Make your Pull Request title in the specification. - Important Prefixes for [release-please](https://github.com/googleapis/release-please): - `fix:` which represents bug fixes, and correlates to a [SemVer](https://semver.org/) patch. - `feat:` represents a new feature, and correlates to a SemVer minor. - `feat!:`, or `fix!:`, `refactor!:`, etc., which represent a breaking change (indicated by the `!`) and will result in a SemVer major. - [x] Ensure the tests and linter pass (Run `bash scripts/format.sh` from the repository root to format) - [x] Appropriate docs were updated (if necessary) # Tests ```zsh uv sync --all-groups ``` Before and after: confirmed that this is fixed. ```zsh uv run python -W error::DeprecationWarning -c "from a2a.server.apps.jsonrpc.fastapi_app import *" ``` Confirmed that tests worked with ```zsh uv run pytest ``` # Other uv.lock currently has ```toml [[package]] name = "starlette" version = "0.50.0" ``` So I don't think the dependency pinning is a breaking change. --------- Co-authored-by: Holt Skinner <13262395+holtskinner@users.noreply.github.com> --- src/a2a/server/apps/jsonrpc/jsonrpc_app.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/a2a/server/apps/jsonrpc/jsonrpc_app.py b/src/a2a/server/apps/jsonrpc/jsonrpc_app.py index 27839cd35..c6f78d119 100644 --- a/src/a2a/server/apps/jsonrpc/jsonrpc_app.py +++ b/src/a2a/server/apps/jsonrpc/jsonrpc_app.py @@ -64,7 +64,14 @@ from starlette.exceptions import HTTPException from starlette.requests import Request from starlette.responses import JSONResponse, Response - from starlette.status import HTTP_413_REQUEST_ENTITY_TOO_LARGE + + try: + # Starlette v0.48.0 + from starlette.status import HTTP_413_CONTENT_TOO_LARGE + except ImportError: + from starlette.status import ( # type: ignore[no-redef] + HTTP_413_REQUEST_ENTITY_TOO_LARGE as HTTP_413_CONTENT_TOO_LARGE, + ) _package_starlette_installed = True else: @@ -76,7 +83,14 @@ from starlette.exceptions import HTTPException from starlette.requests import Request from starlette.responses import JSONResponse, Response - from starlette.status import HTTP_413_REQUEST_ENTITY_TOO_LARGE + + try: + # Starlette v0.48.0 + from starlette.status import HTTP_413_CONTENT_TOO_LARGE + except ImportError: + from starlette.status import ( + HTTP_413_REQUEST_ENTITY_TOO_LARGE as HTTP_413_CONTENT_TOO_LARGE, + ) _package_starlette_installed = True except ImportError: @@ -90,7 +104,7 @@ Request = Any JSONResponse = Any Response = Any - HTTP_413_REQUEST_ENTITY_TOO_LARGE = Any + HTTP_413_CONTENT_TOO_LARGE = Any class StarletteUserProxy(A2AUser): @@ -381,7 +395,7 @@ async def _handle_requests(self, request: Request) -> Response: # noqa: PLR0911 None, A2AError(root=JSONParseError(message=str(e))) ) except HTTPException as e: - if e.status_code == HTTP_413_REQUEST_ENTITY_TOO_LARGE: + if e.status_code == HTTP_413_CONTENT_TOO_LARGE: return self._generate_error_response( request_id, A2AError( From 7632f55572641d8fbc353ee08ef2b1f6b75c38b6 Mon Sep 17 00:00:00 2001 From: Carlos Chinchilla Corbacho <188046461+cchinchilla-dev@users.noreply.github.com> Date: Fri, 20 Feb 2026 10:40:59 +0100 Subject: [PATCH 2/3] fix(core): preserve legitimate falsy values in _clean_empty (#713) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Description Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly: - [x] Follow the [`CONTRIBUTING` Guide](https://github.com/a2aproject/a2a-python/blob/main/CONTRIBUTING.md). - [x] Make your Pull Request title in the specification. - Important Prefixes for [release-please](https://github.com/googleapis/release-please): - `fix:` which represents bug fixes, and correlates to a [SemVer](https://semver.org/) patch. - `feat:` represents a new feature, and correlates to a SemVer minor. - `feat!:`, or `fix!:`, `refactor!:`, etc., which represent a breaking change (indicated by the `!`) and will result in a SemVer major. - [x] Ensure the tests and linter pass (Run `bash scripts/format.sh` from the repository root to format) - [x] Appropriate docs were updated (if necessary) Fixes #692 🦕 ## Problem `_clean_empty` uses a truthiness check (`if v`) that drops legitimate falsy values like `0`, `False`, and `0.0` alongside the intended empties (`''`, `[]`, `{}`). This affects `canonicalize_agent_card()` — the only caller — which produces canonical JSON for signature verification (RFC 8785). An `AgentCard` with e.g. `AgentCapabilities(streaming=False)` would have the field silently stripped, losing the distinction between "explicitly disabled" and "unspecified". ## Fix Restructured `_clean_empty` so that only empty strings are converted to `None` at the leaf level, and empty containers collapse naturally via `or None` after their children are cleaned. The filter becomes `if v is not None`, which only catches values the function itself converted — `0`, `False`, and `0.0` never go through any conversion and flow through unchanged. ### Alternatives considered If maintainers prefer a different style, happy to switch to either: - Inline sentinel: `if v not in (None, '', [], {})` directly in the comprehensions, keeping the original structure with a fixed filter. - Helper predicate: extract an `_is_empty(v)` function and use `if not _is_empty(v)` as the filter in both dict and list comprehensions. ## Tests Parametrized tests covering empty removal, falsy value preservation, mixed cases, non-mutation, and a regression test confirming `streaming=False` survives in `canonicalize_agent_card` output. Happy to trim test cases if seen as excessive. --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/a2a/utils/helpers.py | 18 ++++--- tests/utils/test_helpers.py | 102 ++++++++++++++++++++++++++++++++++-- 2 files changed, 111 insertions(+), 9 deletions(-) diff --git a/src/a2a/utils/helpers.py b/src/a2a/utils/helpers.py index 8164674e5..cfc06c1a8 100644 --- a/src/a2a/utils/helpers.py +++ b/src/a2a/utils/helpers.py @@ -350,14 +350,20 @@ def are_modalities_compatible( def _clean_empty(d: Any) -> Any: """Recursively remove empty strings, lists and dicts from a dictionary.""" if isinstance(d, dict): - cleaned_dict: dict[Any, Any] = { - k: _clean_empty(v) for k, v in d.items() + cleaned_dict = { + k: cleaned_v + for k, v in d.items() + if (cleaned_v := _clean_empty(v)) is not None } - return {k: v for k, v in cleaned_dict.items() if v} + return cleaned_dict or None if isinstance(d, list): - cleaned_list: list[Any] = [_clean_empty(v) for v in d] - return [v for v in cleaned_list if v] - return d if d not in ['', [], {}] else None + cleaned_list = [ + cleaned_v for v in d if (cleaned_v := _clean_empty(v)) is not None + ] + return cleaned_list or None + if isinstance(d, str) and not d: + return None + return d def canonicalize_agent_card(agent_card: AgentCard) -> str: diff --git a/tests/utils/test_helpers.py b/tests/utils/test_helpers.py index f3227d327..dc5e0c000 100644 --- a/tests/utils/test_helpers.py +++ b/tests/utils/test_helpers.py @@ -6,11 +6,11 @@ import pytest from a2a.types import ( - Artifact, + AgentCapabilities, AgentCard, AgentCardSignature, - AgentCapabilities, AgentSkill, + Artifact, Message, MessageSendParams, Part, @@ -22,12 +22,13 @@ ) from a2a.utils.errors import ServerError from a2a.utils.helpers import ( + _clean_empty, append_artifact_to_task, are_modalities_compatible, build_text_artifact, + canonicalize_agent_card, create_task_obj, validate, - canonicalize_agent_card, ) @@ -380,3 +381,98 @@ def test_canonicalize_agent_card(): ) result = canonicalize_agent_card(agent_card) assert result == expected_jcs + + +def test_canonicalize_agent_card_preserves_false_capability(): + """Regression #692: streaming=False must not be stripped from canonical JSON.""" + card = AgentCard( + **{ + **SAMPLE_AGENT_CARD, + 'capabilities': AgentCapabilities( + streaming=False, + push_notifications=True, + ), + } + ) + result = canonicalize_agent_card(card) + assert '"streaming":false' in result + + +@pytest.mark.parametrize( + 'input_val', + [ + pytest.param({'a': ''}, id='empty-string'), + pytest.param({'a': []}, id='empty-list'), + pytest.param({'a': {}}, id='empty-dict'), + pytest.param({'a': {'b': []}}, id='nested-empty'), + pytest.param({'a': '', 'b': [], 'c': {}}, id='all-empties'), + pytest.param({'a': {'b': {'c': ''}}}, id='deeply-nested'), + ], +) +def test_clean_empty_removes_empties(input_val): + """_clean_empty removes empty strings, lists, and dicts recursively.""" + assert _clean_empty(input_val) is None + + +def test_clean_empty_top_level_list_becomes_none(): + """Top-level list that becomes empty after cleaning should return None.""" + assert _clean_empty(['', {}, []]) is None + + +@pytest.mark.parametrize( + 'input_val,expected', + [ + pytest.param({'retries': 0}, {'retries': 0}, id='int-zero'), + pytest.param({'enabled': False}, {'enabled': False}, id='bool-false'), + pytest.param({'score': 0.0}, {'score': 0.0}, id='float-zero'), + pytest.param([0, 1, 2], [0, 1, 2], id='zero-in-list'), + pytest.param([False, True], [False, True], id='false-in-list'), + pytest.param( + {'config': {'max_retries': 0, 'name': 'agent'}}, + {'config': {'max_retries': 0, 'name': 'agent'}}, + id='nested-zero', + ), + ], +) +def test_clean_empty_preserves_falsy_values(input_val, expected): + """_clean_empty preserves legitimate falsy values (0, False, 0.0).""" + assert _clean_empty(input_val) == expected + + +@pytest.mark.parametrize( + 'input_val,expected', + [ + pytest.param( + {'count': 0, 'label': '', 'items': []}, + {'count': 0}, + id='falsy-with-empties', + ), + pytest.param( + {'a': 0, 'b': 'hello', 'c': False, 'd': ''}, + {'a': 0, 'b': 'hello', 'c': False}, + id='mixed-types', + ), + pytest.param( + {'name': 'agent', 'retries': 0, 'tags': [], 'desc': ''}, + {'name': 'agent', 'retries': 0}, + id='realistic-mixed', + ), + ], +) +def test_clean_empty_mixed(input_val, expected): + """_clean_empty handles mixed empty and falsy values correctly.""" + assert _clean_empty(input_val) == expected + + +def test_clean_empty_does_not_mutate_input(): + """_clean_empty should not mutate the original input object.""" + original = {'a': '', 'b': 1, 'c': {'d': ''}} + original_copy = { + 'a': '', + 'b': 1, + 'c': {'d': ''}, + } + + _clean_empty(original) + + assert original == original_copy From d2d38608b0262aeb67d94d11136a4147e65a7351 Mon Sep 17 00:00:00 2001 From: "Agent2Agent (A2A) Bot" Date: Fri, 20 Feb 2026 04:04:52 -0600 Subject: [PATCH 3/3] chore(main): release 0.3.24 (#708) :robot: I have created a release *beep* *boop* --- ## [0.3.24](https://github.com/a2aproject/a2a-python/compare/v0.3.23...v0.3.24) (2026-02-20) ### Bug Fixes * **core:** preserve legitimate falsy values in _clean_empty ([#713](https://github.com/a2aproject/a2a-python/issues/713)) ([7632f55](https://github.com/a2aproject/a2a-python/commit/7632f55572641d8fbc353ee08ef2b1f6b75c38b6)) * **deps:** `DeprecationWarning` on `HTTP_413_REQUEST_ENTITY_TOO_LARGE` ([#693](https://github.com/a2aproject/a2a-python/issues/693)) ([9968f9c](https://github.com/a2aproject/a2a-python/commit/9968f9c07f105bae8a6b296aeb6dea873b3b88b0)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b4fca053a..64191ac7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [0.3.24](https://github.com/a2aproject/a2a-python/compare/v0.3.23...v0.3.24) (2026-02-20) + + +### Bug Fixes + +* **core:** preserve legitimate falsy values in _clean_empty ([#713](https://github.com/a2aproject/a2a-python/issues/713)) ([7632f55](https://github.com/a2aproject/a2a-python/commit/7632f55572641d8fbc353ee08ef2b1f6b75c38b6)) +* **deps:** `DeprecationWarning` on `HTTP_413_REQUEST_ENTITY_TOO_LARGE` ([#693](https://github.com/a2aproject/a2a-python/issues/693)) ([9968f9c](https://github.com/a2aproject/a2a-python/commit/9968f9c07f105bae8a6b296aeb6dea873b3b88b0)) + ## [0.3.23](https://github.com/a2aproject/a2a-python/compare/v0.3.22...v0.3.23) (2026-02-13)