Skip to content

Commit 5813558

Browse files
authored
Merge branch 'main' into split_proto_deps
2 parents b7e2c71 + 89e9b7c commit 5813558

File tree

8 files changed

+103
-25
lines changed

8 files changed

+103
-25
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## [0.3.14](https://github.com/a2aproject/a2a-python/compare/v0.3.13...v0.3.14) (2025-11-17)
4+
5+
6+
### Features
7+
8+
* **jsonrpc:** add option to disable oversized payload check in JSONRPC applications ([ba142df](https://github.com/a2aproject/a2a-python/commit/ba142df821d1c06be0b96e576fd43015120fcb0b))
9+
10+
## [0.3.13](https://github.com/a2aproject/a2a-python/compare/v0.3.12...v0.3.13) (2025-11-13)
11+
12+
13+
### Bug Fixes
14+
15+
* return entire history when history_length=0 ([#537](https://github.com/a2aproject/a2a-python/issues/537)) ([acdc0de](https://github.com/a2aproject/a2a-python/commit/acdc0de4fa03d34a6b287ab252ff51b19c3016b5))
16+
317
## [0.3.12](https://github.com/a2aproject/a2a-python/compare/v0.3.11...v0.3.12) (2025-11-12)
418

519

src/a2a/client/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
CredentialService,
88
InMemoryContextCredentialStore,
99
)
10+
from a2a.client.base_client import BaseClient
1011
from a2a.client.card_resolver import A2ACardResolver
1112
from a2a.client.client import Client, ClientConfig, ClientEvent, Consumer
1213
from a2a.client.client_factory import ClientFactory, minimal_agent_card
@@ -51,6 +52,7 @@ def __init__(self, *args, **kwargs):
5152
'A2AClientTimeoutError',
5253
'A2AGrpcClient',
5354
'AuthInterceptor',
55+
'BaseClient',
5456
'Client',
5557
'ClientCallContext',
5658
'ClientCallInterceptor',

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def __init__( # noqa: PLR0913
7777
[AgentCard, ServerCallContext], AgentCard
7878
]
7979
| None = None,
80+
max_content_length: int | None = 10 * 1024 * 1024, # 10MB
8081
) -> None:
8182
"""Initializes the A2AFastAPIApplication.
8283
@@ -94,6 +95,8 @@ def __init__( # noqa: PLR0913
9495
extended_card_modifier: An optional callback to dynamically modify
9596
the extended agent card before it is served. It receives the
9697
call context.
98+
max_content_length: The maximum allowed content length for incoming
99+
requests. Defaults to 10MB. Set to None for unbounded maximum.
97100
"""
98101
if not _package_fastapi_installed:
99102
raise ImportError(
@@ -108,6 +111,7 @@ def __init__( # noqa: PLR0913
108111
context_builder=context_builder,
109112
card_modifier=card_modifier,
110113
extended_card_modifier=extended_card_modifier,
114+
max_content_length=max_content_length,
111115
)
112116

113117
def add_routes_to_app(

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

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,6 @@
9191
Response = Any
9292
HTTP_413_REQUEST_ENTITY_TOO_LARGE = Any
9393

94-
MAX_CONTENT_LENGTH = 10_000_000
95-
9694

9795
class StarletteUserProxy(A2AUser):
9896
"""Adapts the Starlette User class to the A2A user representation."""
@@ -185,6 +183,7 @@ def __init__( # noqa: PLR0913
185183
[AgentCard, ServerCallContext], AgentCard
186184
]
187185
| None = None,
186+
max_content_length: int | None = 10 * 1024 * 1024, # 10MB
188187
) -> None:
189188
"""Initializes the JSONRPCApplication.
190189
@@ -202,6 +201,8 @@ def __init__( # noqa: PLR0913
202201
extended_card_modifier: An optional callback to dynamically modify
203202
the extended agent card before it is served. It receives the
204203
call context.
204+
max_content_length: The maximum allowed content length for incoming
205+
requests. Defaults to 10MB. Set to None for unbounded maximum.
205206
"""
206207
if not _package_starlette_installed:
207208
raise ImportError(
@@ -220,6 +221,7 @@ def __init__( # noqa: PLR0913
220221
extended_card_modifier=extended_card_modifier,
221222
)
222223
self._context_builder = context_builder or DefaultCallContextBuilder()
224+
self._max_content_length = max_content_length
223225

224226
def _generate_error_response(
225227
self, request_id: str | int | None, error: JSONRPCError | A2AError
@@ -261,6 +263,22 @@ def _generate_error_response(
261263
status_code=200,
262264
)
263265

266+
def _allowed_content_length(self, request: Request) -> bool:
267+
"""Checks if the request content length is within the allowed maximum.
268+
269+
Args:
270+
request: The incoming Starlette Request object.
271+
272+
Returns:
273+
False if the content length is larger than the allowed maximum, True otherwise.
274+
"""
275+
if self._max_content_length is not None:
276+
with contextlib.suppress(ValueError):
277+
content_length = int(request.headers.get('content-length', '0'))
278+
if content_length and content_length > self._max_content_length:
279+
return False
280+
return True
281+
264282
async def _handle_requests(self, request: Request) -> Response: # noqa: PLR0911
265283
"""Handles incoming POST requests to the main A2A endpoint.
266284
@@ -291,18 +309,14 @@ async def _handle_requests(self, request: Request) -> Response: # noqa: PLR0911
291309
request_id, str | int
292310
):
293311
request_id = None
294-
# Treat very large payloads as invalid request (-32600) before routing
295-
with contextlib.suppress(Exception):
296-
content_length = int(request.headers.get('content-length', '0'))
297-
if content_length and content_length > MAX_CONTENT_LENGTH:
298-
return self._generate_error_response(
299-
request_id,
300-
A2AError(
301-
root=InvalidRequestError(
302-
message='Payload too large'
303-
)
304-
),
305-
)
312+
# Treat payloads lager than allowed as invalid request (-32600) before routing
313+
if not self._allowed_content_length(request):
314+
return self._generate_error_response(
315+
request_id,
316+
A2AError(
317+
root=InvalidRequestError(message='Payload too large')
318+
),
319+
)
306320
logger.debug('Request body: %s', body)
307321
# 1) Validate base JSON-RPC structure only (-32600 on failure)
308322
try:

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def __init__( # noqa: PLR0913
5959
[AgentCard, ServerCallContext], AgentCard
6060
]
6161
| None = None,
62+
max_content_length: int | None = 10 * 1024 * 1024, # 10MB
6263
) -> None:
6364
"""Initializes the A2AStarletteApplication.
6465
@@ -76,6 +77,8 @@ def __init__( # noqa: PLR0913
7677
extended_card_modifier: An optional callback to dynamically modify
7778
the extended agent card before it is served. It receives the
7879
call context.
80+
max_content_length: The maximum allowed content length for incoming
81+
requests. Defaults to 10MB. Set to None for unbounded maximum.
7982
"""
8083
if not _package_starlette_installed:
8184
raise ImportError(
@@ -90,6 +93,7 @@ def __init__( # noqa: PLR0913
9093
context_builder=context_builder,
9194
card_modifier=card_modifier,
9295
extended_card_modifier=extended_card_modifier,
96+
max_content_length=max_content_length,
9397
)
9498

9599
def routes(

src/a2a/utils/task.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,9 @@ def apply_history_length(task: Task, history_length: int | None) -> Task:
8383
A new task object with limited history
8484
"""
8585
# Apply historyLength parameter if specified
86-
if history_length is not None and task.history:
86+
if history_length is not None and history_length > 0 and task.history:
8787
# Limit history to the most recent N messages
88-
limited_history = (
89-
task.history[-history_length:] if history_length > 0 else []
90-
)
88+
limited_history = task.history[-history_length:]
9189
# Create a new task instance with limited history
9290
return task.model_copy(update={'history': limited_history})
9391

tests/server/apps/jsonrpc/test_serialization.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,42 @@ def test_handle_oversized_payload(agent_card_with_api_key: AgentCard):
136136
assert data['error']['code'] == InvalidRequestError().code
137137

138138

139+
@pytest.mark.parametrize(
140+
'max_content_length',
141+
[
142+
None,
143+
11 * 1024 * 1024,
144+
30 * 1024 * 1024,
145+
],
146+
)
147+
def test_handle_oversized_payload_with_max_content_length(
148+
agent_card_with_api_key: AgentCard,
149+
max_content_length: int | None,
150+
):
151+
"""Test handling of JSON payloads with sizes within custom max_content_length."""
152+
handler = mock.AsyncMock()
153+
app_instance = A2AStarletteApplication(
154+
agent_card_with_api_key, handler, max_content_length=max_content_length
155+
)
156+
client = TestClient(app_instance.build())
157+
158+
large_string = 'a' * 11 * 1_000_000 # 11MB string
159+
payload = {
160+
'jsonrpc': '2.0',
161+
'method': 'test',
162+
'id': 1,
163+
'params': {'data': large_string},
164+
}
165+
166+
response = client.post('/', json=payload)
167+
assert response.status_code == 200
168+
data = response.json()
169+
# When max_content_length is set, requests up to that size should not be
170+
# rejected due to payload size. The request might fail for other reasons,
171+
# but it shouldn't be an InvalidRequestError related to the content length.
172+
assert data['error']['code'] != InvalidRequestError().code
173+
174+
139175
def test_handle_unicode_characters(agent_card_with_api_key: AgentCard):
140176
"""Test handling of unicode characters in JSON payload."""
141177
handler = mock.AsyncMock()

tests/server/request_handlers/test_default_request_handler.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -834,6 +834,11 @@ async def test_on_message_send_non_blocking():
834834

835835
assert task is not None
836836
assert task.status.state == TaskState.completed
837+
assert (
838+
result.history
839+
and task.history
840+
and len(result.history) == len(task.history)
841+
)
837842

838843

839844
@pytest.mark.asyncio
@@ -855,7 +860,7 @@ async def test_on_message_send_limit_history():
855860
configuration=MessageSendConfiguration(
856861
blocking=True,
857862
accepted_output_modes=['text/plain'],
858-
history_length=0,
863+
history_length=1,
859864
),
860865
)
861866

@@ -866,17 +871,17 @@ async def test_on_message_send_limit_history():
866871
# verify that history_length is honored
867872
assert result is not None
868873
assert isinstance(result, Task)
869-
assert result.history is not None and len(result.history) == 0
874+
assert result.history is not None and len(result.history) == 1
870875
assert result.status.state == TaskState.completed
871876

872877
# verify that history is still persisted to the store
873878
task = await task_store.get(result.id)
874879
assert task is not None
875-
assert task.history is not None and len(task.history) > 0
880+
assert task.history is not None and len(task.history) > 1
876881

877882

878883
@pytest.mark.asyncio
879-
async def test_on_task_get_limit_history():
884+
async def test_on_get_task_limit_history():
880885
task_store = InMemoryTaskStore()
881886
push_store = InMemoryPushNotificationConfigStore()
882887

@@ -892,7 +897,8 @@ async def test_on_task_get_limit_history():
892897
parts=[Part(root=TextPart(text='Hi'))],
893898
),
894899
configuration=MessageSendConfiguration(
895-
blocking=True, accepted_output_modes=['text/plain']
900+
blocking=True,
901+
accepted_output_modes=['text/plain'],
896902
),
897903
)
898904

@@ -904,14 +910,14 @@ async def test_on_task_get_limit_history():
904910
assert isinstance(result, Task)
905911

906912
get_task_result = await request_handler.on_get_task(
907-
TaskQueryParams(id=result.id, history_length=0),
913+
TaskQueryParams(id=result.id, history_length=1),
908914
create_server_call_context(),
909915
)
910916
assert get_task_result is not None
911917
assert isinstance(get_task_result, Task)
912918
assert (
913919
get_task_result.history is not None
914-
and len(get_task_result.history) == 0
920+
and len(get_task_result.history) == 1
915921
)
916922

917923

0 commit comments

Comments
 (0)