From ebf2accea218383ba8ec4e073fe3e5de12c2ed32 Mon Sep 17 00:00:00 2001 From: meraklbz Date: Mon, 10 Aug 2026 20:05:15 +0800 Subject: [PATCH 1/2] fix: enforce upper bound on history_length --- src/a2a/utils/task.py | 17 +++++- .../test_default_request_handler.py | 53 +++++++++++++++++++ .../test_default_request_handler_v2.py | 21 ++++++++ tests/utils/test_task.py | 32 +++++++++++ 4 files changed, 122 insertions(+), 1 deletion(-) diff --git a/src/a2a/utils/task.py b/src/a2a/utils/task.py index 4acf54e46..7ce3e8333 100644 --- a/src/a2a/utils/task.py +++ b/src/a2a/utils/task.py @@ -10,6 +10,17 @@ from a2a.utils.errors import InvalidParamsError +MAX_HISTORY_LENGTH = 1000 +"""Maximum allowed value for a ``history_length`` request. + +Keeps a single request from asking the server to materialize an +unbounded history (e.g. ``historyLength=999999999``). The value is a +pragmatic cap aligned with the other A2A SDKs' semantics, where very +large values effectively mean "return everything available"; clients +requesting more than this cap get ``InvalidParamsError``. +""" + + @runtime_checkable class HistoryLengthConfig(Protocol): """Protocol for configuration arguments containing history_length field.""" @@ -25,9 +36,13 @@ def HasField(self, field_name: Literal['history_length']) -> bool: # noqa: N802 def validate_history_length(config: HistoryLengthConfig | None) -> None: - """Validates that history_length is non-negative.""" + """Validates that history_length is non-negative and within limits.""" if config and config.history_length < 0: raise InvalidParamsError(message='history length must be non-negative') + if config and config.history_length > MAX_HISTORY_LENGTH: + raise InvalidParamsError( + message=f'history length must be at most {MAX_HISTORY_LENGTH}' + ) def apply_history_length( diff --git a/tests/server/request_handlers/test_default_request_handler.py b/tests/server/request_handlers/test_default_request_handler.py index 727679e7c..6692895f9 100644 --- a/tests/server/request_handlers/test_default_request_handler.py +++ b/tests/server/request_handlers/test_default_request_handler.py @@ -2796,6 +2796,59 @@ async def test_on_get_task_negative_history_length_error(agent_card): assert 'history length must be non-negative' in exc_info.value.message +@pytest.mark.asyncio +async def test_on_get_task_history_length_too_large_error(agent_card): + """Test on_get_task raises error for history length above the limit.""" + from a2a.utils.task import MAX_HISTORY_LENGTH + + mock_task_store = AsyncMock(spec=TaskStore) + request_handler = DefaultRequestHandler( + agent_executor=AsyncMock(spec=AgentExecutor), + task_store=mock_task_store, + agent_card=agent_card, + ) + params = GetTaskRequest( + id='task1', history_length=MAX_HISTORY_LENGTH + 1 + ) + context = create_server_call_context() + + with pytest.raises(InvalidParamsError) as exc_info: + await request_handler.on_get_task(params, context) + + assert str(MAX_HISTORY_LENGTH) in exc_info.value.message + mock_task_store.get.assert_not_awaited() + + +@pytest.mark.asyncio +async def test_on_message_send_history_length_too_large_error(agent_card): + """Test on_message_send raises error for history length above the limit.""" + from a2a.utils.task import MAX_HISTORY_LENGTH + + mock_task_store = AsyncMock(spec=TaskStore) + request_handler = DefaultRequestHandler( + agent_executor=AsyncMock(spec=AgentExecutor), + task_store=mock_task_store, + agent_card=agent_card, + ) + + message_config = SendMessageConfiguration( + history_length=MAX_HISTORY_LENGTH + 1, + accepted_output_modes=['text/plain'], + ) + params = SendMessageRequest( + message=Message( + role=Role.ROLE_USER, message_id='msg1', parts=[Part(text='Test')] + ), + configuration=message_config, + ) + context = create_server_call_context() + + with pytest.raises(InvalidParamsError) as exc_info: + await request_handler.on_message_send(params, context) + + assert str(MAX_HISTORY_LENGTH) in exc_info.value.message + + @pytest.mark.asyncio async def test_on_list_tasks_page_size_too_small(agent_card): """Test on_list_tasks raises error for page_size < 1.""" diff --git a/tests/server/request_handlers/test_default_request_handler_v2.py b/tests/server/request_handlers/test_default_request_handler_v2.py index b276fb77a..7127c62a6 100644 --- a/tests/server/request_handlers/test_default_request_handler_v2.py +++ b/tests/server/request_handlers/test_default_request_handler_v2.py @@ -1075,6 +1075,27 @@ async def test_on_get_task_negative_history_length_error(): assert 'history length must be non-negative' in exc_info.value.message +@pytest.mark.asyncio +async def test_on_get_task_history_length_too_large_error(): + """Test on_get_task raises error for history length above the limit.""" + from a2a.utils.task import MAX_HISTORY_LENGTH + + mock_task_store = AsyncMock(spec=TaskStore) + request_handler = DefaultRequestHandlerV2( + agent_executor=AsyncMock(spec=AgentExecutor), + task_store=mock_task_store, + agent_card=create_default_agent_card(), + ) + params = GetTaskRequest( + id='task1', history_length=MAX_HISTORY_LENGTH + 1 + ) + context = create_server_call_context() + with pytest.raises(InvalidParamsError) as exc_info: + await request_handler.on_get_task(params, context) + assert str(MAX_HISTORY_LENGTH) in exc_info.value.message + mock_task_store.get.assert_not_awaited() + + @pytest.mark.asyncio async def test_on_list_tasks_page_size_too_small(): """Test on_list_tasks raises error for page_size < 1.""" diff --git a/tests/utils/test_task.py b/tests/utils/test_task.py index 8124955d1..5cd9ce3bc 100644 --- a/tests/utils/test_task.py +++ b/tests/utils/test_task.py @@ -14,9 +14,11 @@ ) from a2a.utils.errors import InvalidParamsError from a2a.utils.task import ( + MAX_HISTORY_LENGTH, apply_history_length, decode_page_token, encode_page_token, + validate_history_length, ) @@ -89,5 +91,35 @@ def test_zero_history_length_returns_empty_history(self): self.assertEqual(len(result.history), 0) +class TestValidateHistoryLength(unittest.TestCase): + def test_none_config_passes(self): + # Does not raise + validate_history_length(None) + + def test_zero_passes(self): + validate_history_length(GetTaskRequest(history_length=0)) + + def test_boundary_max_passes(self): + validate_history_length(GetTaskRequest(history_length=MAX_HISTORY_LENGTH)) + + def test_negative_raises(self): + with pytest.raises(InvalidParamsError) as excinfo: + validate_history_length(GetTaskRequest(history_length=-1)) + assert 'non-negative' in str(excinfo.value) + + def test_over_max_raises(self): + with pytest.raises(InvalidParamsError) as excinfo: + validate_history_length( + GetTaskRequest(history_length=MAX_HISTORY_LENGTH + 1) + ) + assert str(MAX_HISTORY_LENGTH) in str(excinfo.value) + + def test_over_max_raises_for_send_configuration(self): + with pytest.raises(InvalidParamsError): + validate_history_length( + SendMessageConfiguration(history_length=MAX_HISTORY_LENGTH + 1) + ) + + if __name__ == '__main__': unittest.main() From 79f28e9d027a2e9e46f142c3c77339bc4f7f8fcb Mon Sep 17 00:00:00 2001 From: meraklbz Date: Tue, 11 Aug 2026 00:39:38 +0800 Subject: [PATCH 2/2] style: apply ruff formatting --- tests/server/request_handlers/test_default_request_handler.py | 4 +--- .../request_handlers/test_default_request_handler_v2.py | 4 +--- tests/utils/test_task.py | 4 +++- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/server/request_handlers/test_default_request_handler.py b/tests/server/request_handlers/test_default_request_handler.py index 6692895f9..1ce65e2f0 100644 --- a/tests/server/request_handlers/test_default_request_handler.py +++ b/tests/server/request_handlers/test_default_request_handler.py @@ -2807,9 +2807,7 @@ async def test_on_get_task_history_length_too_large_error(agent_card): task_store=mock_task_store, agent_card=agent_card, ) - params = GetTaskRequest( - id='task1', history_length=MAX_HISTORY_LENGTH + 1 - ) + params = GetTaskRequest(id='task1', history_length=MAX_HISTORY_LENGTH + 1) context = create_server_call_context() with pytest.raises(InvalidParamsError) as exc_info: diff --git a/tests/server/request_handlers/test_default_request_handler_v2.py b/tests/server/request_handlers/test_default_request_handler_v2.py index 7127c62a6..404ef3a1c 100644 --- a/tests/server/request_handlers/test_default_request_handler_v2.py +++ b/tests/server/request_handlers/test_default_request_handler_v2.py @@ -1086,9 +1086,7 @@ async def test_on_get_task_history_length_too_large_error(): task_store=mock_task_store, agent_card=create_default_agent_card(), ) - params = GetTaskRequest( - id='task1', history_length=MAX_HISTORY_LENGTH + 1 - ) + params = GetTaskRequest(id='task1', history_length=MAX_HISTORY_LENGTH + 1) context = create_server_call_context() with pytest.raises(InvalidParamsError) as exc_info: await request_handler.on_get_task(params, context) diff --git a/tests/utils/test_task.py b/tests/utils/test_task.py index 5cd9ce3bc..5dde3b447 100644 --- a/tests/utils/test_task.py +++ b/tests/utils/test_task.py @@ -100,7 +100,9 @@ def test_zero_passes(self): validate_history_length(GetTaskRequest(history_length=0)) def test_boundary_max_passes(self): - validate_history_length(GetTaskRequest(history_length=MAX_HISTORY_LENGTH)) + validate_history_length( + GetTaskRequest(history_length=MAX_HISTORY_LENGTH) + ) def test_negative_raises(self): with pytest.raises(InvalidParamsError) as excinfo: