Skip to content

Commit 54ea223

Browse files
authored
Merge branch 'main' into feat/add-base-model
2 parents a32b718 + 07e9b84 commit 54ea223

6 files changed

Lines changed: 243 additions & 104 deletions

File tree

.github/workflows/unit-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,6 @@ jobs:
4545
- name: Install dependencies
4646
run: uv sync --dev --extra sql
4747
- name: Run tests and check coverage
48-
run: uv run pytest --cov=a2a --cov-report=xml --cov-fail-under=90
48+
run: uv run pytest --cov=a2a --cov-report=xml --cov-fail-under=89
4949
- name: Show coverage summary in log
5050
run: uv run coverage report

src/a2a/grpc/a2a_pb2.py

Lines changed: 100 additions & 100 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/a2a/server/apps/jsonrpc/jsonrpc_app.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212
from sse_starlette.sse import EventSourceResponse
1313
from starlette.applications import Starlette
1414
from starlette.authentication import BaseUser
15+
from starlette.exceptions import HTTPException
1516
from starlette.requests import Request
1617
from starlette.responses import JSONResponse, Response
18+
from starlette.status import HTTP_413_REQUEST_ENTITY_TOO_LARGE
1719

1820
from a2a.auth.user import UnauthenticatedUser
1921
from a2a.auth.user import User as A2AUser
@@ -45,6 +47,7 @@
4547
from a2a.utils.constants import (
4648
AGENT_CARD_WELL_KNOWN_PATH,
4749
DEFAULT_RPC_URL,
50+
EXTENDED_AGENT_CARD_PATH,
4851
)
4952
from a2a.utils.errors import MethodNotImplementedError
5053

@@ -176,7 +179,7 @@ def _generate_error_response(
176179
status_code=200,
177180
)
178181

179-
async def _handle_requests(self, request: Request) -> Response:
182+
async def _handle_requests(self, request: Request) -> Response: # noqa: PLR0911
180183
"""Handles incoming POST requests to the main A2A endpoint.
181184
182185
Parses the request body as JSON, validates it against A2A request types,
@@ -232,6 +235,15 @@ async def _handle_requests(self, request: Request) -> Response:
232235
request_id,
233236
A2AError(root=InvalidRequestError(data=json.loads(e.json()))),
234237
)
238+
except HTTPException as e:
239+
if e.status_code == HTTP_413_REQUEST_ENTITY_TOO_LARGE:
240+
return self._generate_error_response(
241+
request_id,
242+
A2AError(
243+
root=InvalidRequestError(message='Payload too large')
244+
),
245+
)
246+
raise e
235247
except Exception as e:
236248
logger.error(f'Unhandled exception: {e}')
237249
traceback.print_exc()
@@ -438,13 +450,16 @@ def build(
438450
self,
439451
agent_card_url: str = AGENT_CARD_WELL_KNOWN_PATH,
440452
rpc_url: str = DEFAULT_RPC_URL,
453+
extended_agent_card_url: str = EXTENDED_AGENT_CARD_PATH,
441454
**kwargs: Any,
442455
) -> FastAPI | Starlette:
443456
"""Builds and returns the JSONRPC application instance.
444457
445458
Args:
446459
agent_card_url: The URL for the agent card endpoint.
447-
rpc_url: The URL for the A2A JSON-RPC endpoint
460+
rpc_url: The URL for the A2A JSON-RPC endpoint.
461+
extended_agent_card_url: The URL for the authenticated extended
462+
agent card endpoint.
448463
**kwargs: Additional keyword arguments to pass to the FastAPI constructor.
449464
450465
Returns:

src/a2a/server/events/event_consumer.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
from collections.abc import AsyncGenerator
66

7+
from pydantic import ValidationError
8+
79
from a2a.server.events.event_queue import Event, EventQueue
810
from a2a.types import (
911
InternalError,
@@ -138,6 +140,9 @@ async def consume_all(self) -> AsyncGenerator[Event]:
138140
# python 3.12 and get a queue empty error on an open queue
139141
if self.queue.is_closed():
140142
break
143+
except ValidationError as e:
144+
logger.error(f'Invalid event format received: {e}')
145+
continue
141146
except Exception as e:
142147
logger.error(
143148
f'Stopping event consumption due to exception: {e}'

tests/server/apps/jsonrpc/test_serialization.py

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from unittest import mock
22

33
import pytest
4+
5+
from pydantic import ValidationError
46
from starlette.testclient import TestClient
57

68
from a2a.server.apps import A2AFastAPIApplication, A2AStarletteApplication
@@ -9,9 +11,14 @@
911
AgentCapabilities,
1012
AgentCard,
1113
In,
14+
InvalidRequestError,
15+
JSONParseError,
16+
Message,
17+
Part,
18+
Role,
1219
SecurityScheme,
20+
TextPart,
1321
)
14-
from pydantic import ValidationError
1522

1623

1724
@pytest.fixture
@@ -92,3 +99,88 @@ def test_fastapi_agent_card_with_api_key_scheme_alias(
9299
assert 'in' in security_scheme_json
93100
assert 'in_' not in security_scheme_json
94101
assert security_scheme_json['in'] == 'header'
102+
103+
104+
def test_handle_invalid_json(agent_card_with_api_key: AgentCard):
105+
"""Test handling of malformed JSON."""
106+
handler = mock.AsyncMock()
107+
app_instance = A2AStarletteApplication(agent_card_with_api_key, handler)
108+
client = TestClient(app_instance.build())
109+
110+
response = client.post(
111+
'/',
112+
content='{ "jsonrpc": "2.0", "method": "test", "id": 1, "params": { "key": "value" }',
113+
)
114+
assert response.status_code == 200
115+
data = response.json()
116+
assert data['error']['code'] == JSONParseError().code
117+
118+
119+
def test_handle_oversized_payload(agent_card_with_api_key: AgentCard):
120+
"""Test handling of oversized JSON payloads."""
121+
handler = mock.AsyncMock()
122+
app_instance = A2AStarletteApplication(agent_card_with_api_key, handler)
123+
client = TestClient(app_instance.build())
124+
125+
large_string = 'a' * 2_000_000 # 2MB string
126+
payload = {
127+
'jsonrpc': '2.0',
128+
'method': 'test',
129+
'id': 1,
130+
'params': {'data': large_string},
131+
}
132+
133+
# Starlette/FastAPI's default max request size is around 1MB.
134+
# This test will likely fail with a 413 Payload Too Large if the default is not increased.
135+
# If the application is expected to handle larger payloads, the server configuration needs to be adjusted.
136+
# For this test, we expect a 413 or a graceful JSON-RPC error if the app handles it.
137+
138+
try:
139+
response = client.post('/', json=payload)
140+
# If the app handles it gracefully and returns a JSON-RPC error
141+
if response.status_code == 200:
142+
data = response.json()
143+
assert data['error']['code'] == InvalidRequestError().code
144+
else:
145+
assert response.status_code == 413
146+
except Exception as e:
147+
# Depending on server setup, it might just drop the connection for very large payloads
148+
assert isinstance(e, (ConnectionResetError, RuntimeError))
149+
150+
151+
def test_handle_unicode_characters(agent_card_with_api_key: AgentCard):
152+
"""Test handling of unicode characters in JSON payload."""
153+
handler = mock.AsyncMock()
154+
app_instance = A2AStarletteApplication(agent_card_with_api_key, handler)
155+
client = TestClient(app_instance.build())
156+
157+
unicode_text = 'こんにちは世界' # "Hello world" in Japanese
158+
unicode_payload = {
159+
'jsonrpc': '2.0',
160+
'method': 'message/send',
161+
'id': 'unicode_test',
162+
'params': {
163+
'message': {
164+
'role': 'user',
165+
'parts': [{'kind': 'text', 'text': unicode_text}],
166+
'messageId': 'msg-unicode',
167+
}
168+
},
169+
}
170+
171+
# Mock a handler for this method
172+
handler.on_message_send.return_value = Message(
173+
role=Role.agent,
174+
parts=[Part(root=TextPart(text=f'Received: {unicode_text}'))],
175+
messageId='response-unicode',
176+
)
177+
178+
response = client.post('/', json=unicode_payload)
179+
180+
# We are not testing the handler logic here, just that the server can correctly
181+
# deserialize the unicode payload without errors. A 200 response with any valid
182+
# JSON-RPC response indicates success.
183+
assert response.status_code == 200
184+
data = response.json()
185+
assert 'error' not in data or data['error'] is None
186+
assert data['result']['parts'][0]['text'] == f'Received: {unicode_text}'

tests/server/events/test_event_consumer.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
import pytest
77

8+
from pydantic import ValidationError
9+
810
from a2a.server.events.event_consumer import EventConsumer, QueueClosed
911
from a2a.server.events.event_queue import EventQueue
1012
from a2a.types import (
@@ -343,3 +345,28 @@ def test_agent_task_callback_no_exception(event_consumer: EventConsumer):
343345

344346
assert event_consumer._exception is None # Should remain None
345347
mock_task.exception.assert_called_once()
348+
349+
350+
@pytest.mark.asyncio
351+
async def test_consume_all_handles_validation_error(
352+
event_consumer: EventConsumer, mock_event_queue: AsyncMock
353+
):
354+
"""Test that consume_all gracefully handles a pydantic.ValidationError."""
355+
# Simulate dequeue_event raising a ValidationError
356+
mock_event_queue.dequeue_event.side_effect = [
357+
ValidationError.from_exception_data(title='Test Error', line_errors=[]),
358+
asyncio.CancelledError, # To stop the loop for the test
359+
]
360+
361+
with patch(
362+
'a2a.server.events.event_consumer.logger.error'
363+
) as logger_error_mock:
364+
with pytest.raises(asyncio.CancelledError):
365+
async for _ in event_consumer.consume_all():
366+
pass
367+
368+
# Check that the specific error was logged and the consumer continued
369+
logger_error_mock.assert_called_once()
370+
assert (
371+
'Invalid event format received' in logger_error_mock.call_args[0][0]
372+
)

0 commit comments

Comments
 (0)