Skip to content

Commit a102d31

Browse files
fix: taskslist error on invalid page token and response serialization (#814)
# Description This PR addresses two main issues related to the list_tasks JSON-RPC endpoint: - Fixed how list_tasks formats the ListTasksResponse to ensure all fields are explicitly printed, even when they carry default values (such as an empty next_page_token). - Replaced generic ValueError exceptions with A2A SDK's standard InvalidParamsError when handling malformed or invalid pagination tokens. This correctly surfaces -32602 Invalid params via JSON-RPC.
1 parent 494a92c commit a102d31

8 files changed

Lines changed: 47 additions & 8 deletions

File tree

src/a2a/server/request_handlers/jsonrpc_handler.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,11 @@ async def list_tasks(
373373
response = await self.request_handler.on_list_tasks(
374374
request, context
375375
)
376-
result = MessageToDict(response, preserving_proto_field_name=False)
376+
result = MessageToDict(
377+
response,
378+
preserving_proto_field_name=False,
379+
always_print_fields_with_no_presence=True,
380+
)
377381
return _build_success_response(request_id, result)
378382
except A2AError as e:
379383
return _build_error_response(request_id, e)

src/a2a/server/tasks/database_task_store.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from a2a.types import a2a_pb2
4242
from a2a.types.a2a_pb2 import Task
4343
from a2a.utils.constants import DEFAULT_LIST_TASKS_PAGE_SIZE
44+
from a2a.utils.errors import InvalidParamsError
4445
from a2a.utils.task import decode_page_token, encode_page_token
4546

4647

@@ -285,7 +286,9 @@ async def list(
285286
)
286287
).scalar_one_or_none()
287288
if not start_task:
288-
raise ValueError(f'Invalid page token: {params.page_token}')
289+
raise InvalidParamsError(
290+
f'Invalid page token: {params.page_token}'
291+
)
289292

290293
start_task_timestamp = start_task.last_updated
291294
where_clauses = []

src/a2a/server/tasks/inmemory_task_store.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from a2a.types import a2a_pb2
88
from a2a.types.a2a_pb2 import Task
99
from a2a.utils.constants import DEFAULT_LIST_TASKS_PAGE_SIZE
10+
from a2a.utils.errors import InvalidParamsError
1011
from a2a.utils.task import decode_page_token, encode_page_token
1112

1213

@@ -135,7 +136,9 @@ async def list(
135136
valid_token = True
136137
break
137138
if not valid_token:
138-
raise ValueError(f'Invalid page token: {params.page_token}')
139+
raise InvalidParamsError(
140+
f'Invalid page token: {params.page_token}'
141+
)
139142
page_size = params.page_size or DEFAULT_LIST_TASKS_PAGE_SIZE
140143
end_idx = start_idx + page_size
141144
next_page_token = (

src/a2a/utils/task.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,9 +186,10 @@ def decode_page_token(page_token: str) -> str:
186186
missing_padding = len(encoded_str) % 4
187187
if missing_padding:
188188
encoded_str += '=' * (4 - missing_padding)
189-
print(f'input: {encoded_str}')
190189
try:
191190
decoded = b64decode(encoded_str.encode(_ENCODING)).decode(_ENCODING)
192191
except (binascii.Error, UnicodeDecodeError) as e:
193-
raise ValueError('Token is not a valid base64-encoded cursor.') from e
192+
raise InvalidParamsError(
193+
'Token is not a valid base64-encoded cursor.'
194+
) from e
194195
return decoded

tests/server/request_handlers/test_jsonrpc_handler.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,31 @@ async def test_on_list_tasks_error(self) -> None:
214214
self.assertTrue(is_error_response(response))
215215
self.assertEqual(response['error']['message'], 'DB down')
216216

217+
async def test_on_list_tasks_empty(self) -> None:
218+
request_handler = AsyncMock(spec=DefaultRequestHandler)
219+
handler = JSONRPCHandler(self.mock_agent_card, request_handler)
220+
221+
mock_result = ListTasksResponse(page_size=10)
222+
request_handler.on_list_tasks.return_value = mock_result
223+
from a2a.types.a2a_pb2 import ListTasksRequest
224+
225+
request = ListTasksRequest(page_size=10)
226+
call_context = ServerCallContext(state={'foo': 'bar'})
227+
228+
response = await handler.list_tasks(request, call_context)
229+
230+
request_handler.on_list_tasks.assert_awaited_once()
231+
self.assertIsInstance(response, dict)
232+
self.assertTrue(is_success_response(response))
233+
self.assertIn('tasks', response['result'])
234+
self.assertEqual(len(response['result']['tasks']), 0)
235+
self.assertIn('nextPageToken', response['result'])
236+
self.assertEqual(response['result']['nextPageToken'], '')
237+
self.assertIn('pageSize', response['result'])
238+
self.assertEqual(response['result']['pageSize'], 10)
239+
self.assertIn('totalSize', response['result'])
240+
self.assertEqual(response['result']['totalSize'], 0)
241+
217242
async def test_on_cancel_task_success(self) -> None:
218243
mock_agent_executor = AsyncMock(spec=AgentExecutor)
219244
mock_task_store = AsyncMock(spec=TaskStore)

tests/server/tasks/test_database_task_store.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from a2a.auth.user import User
3737
from a2a.server.context import ServerCallContext
3838
from a2a.utils.constants import DEFAULT_LIST_TASKS_PAGE_SIZE
39+
from a2a.utils.errors import InvalidParamsError
3940

4041

4142
class SampleUser(User):
@@ -380,7 +381,7 @@ async def test_list_tasks_fails(
380381
for task in tasks_to_create:
381382
await db_store_parameterized.save(task)
382383

383-
with pytest.raises(ValueError) as excinfo:
384+
with pytest.raises(InvalidParamsError) as excinfo:
384385
await db_store_parameterized.list(params)
385386

386387
assert expected_error_message in str(excinfo.value)

tests/server/tasks/test_inmemory_task_store.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from a2a.server.tasks import InMemoryTaskStore
66
from a2a.types.a2a_pb2 import Task, TaskState, TaskStatus, ListTasksRequest
77
from a2a.utils.constants import DEFAULT_LIST_TASKS_PAGE_SIZE
8+
from a2a.utils.errors import InvalidParamsError
89

910
from a2a.auth.user import User
1011

@@ -239,7 +240,7 @@ async def test_list_tasks_fails(
239240
for task in tasks_to_create:
240241
await store.save(task)
241242

242-
with pytest.raises(ValueError) as excinfo:
243+
with pytest.raises(InvalidParamsError) as excinfo:
243244
await store.list(params)
244245

245246
assert expected_error_message in str(excinfo.value)

tests/utils/test_task.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
encode_page_token,
2222
new_task,
2323
)
24+
from a2a.utils.errors import InvalidParamsError
2425

2526

2627
class TestTask(unittest.TestCase):
@@ -214,7 +215,7 @@ def test_decode_page_token_succeeds(self):
214215
assert decode_page_token(self.encoded_page_token) == self.page_token
215216

216217
def test_decode_page_token_fails(self):
217-
with pytest.raises(ValueError) as excinfo:
218+
with pytest.raises(InvalidParamsError) as excinfo:
218219
decode_page_token('invalid')
219220

220221
assert 'Token is not a valid base64-encoded cursor.' in str(

0 commit comments

Comments
 (0)