Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/a2a/server/apps/jsonrpc/fastapi_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,35 +66,38 @@
(SSE).
"""

def __init__( # noqa: PLR0913
self,
agent_card: AgentCard,
http_handler: RequestHandler,
extended_agent_card: AgentCard | None = None,
context_builder: CallContextBuilder | None = None,
card_modifier: Callable[[AgentCard], AgentCard] | None = None,
extended_card_modifier: Callable[
[AgentCard, ServerCallContext], AgentCard
]
| None = None,
max_content_length: int | None = 10 * 1024 * 1024, # 10MB
) -> None:
"""Initializes the A2AFastAPIApplication.

Args:
agent_card: The AgentCard describing the agent's capabilities.
http_handler: The handler instance responsible for processing A2A
requests via http.
extended_agent_card: An optional, distinct AgentCard to be served
at the authenticated extended card endpoint.
context_builder: The CallContextBuilder used to construct the
ServerCallContext passed to the http_handler. If None, no
ServerCallContext is passed.
card_modifier: An optional callback to dynamically modify the public
agent card before it is served.
extended_card_modifier: An optional callback to dynamically modify
the extended agent card before it is served. It receives the
call context.
max_content_length: The maximum allowed content length for incoming
requests. Defaults to 10MB. Set to None for unbounded maximum.
"""

Check notice on line 100 in src/a2a/server/apps/jsonrpc/fastapi_app.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Copy/pasted code

see src/a2a/server/apps/rest/rest_adapter.py (55-82)
if not _package_fastapi_installed:
raise ImportError(
'The `fastapi` package is required to use the `A2AFastAPIApplication`.'
Expand All @@ -108,6 +111,7 @@
context_builder=context_builder,
card_modifier=card_modifier,
extended_card_modifier=extended_card_modifier,
max_content_length=max_content_length,
)

def add_routes_to_app(
Expand Down
44 changes: 29 additions & 15 deletions src/a2a/server/apps/jsonrpc/jsonrpc_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,6 @@
Response = Any
HTTP_413_REQUEST_ENTITY_TOO_LARGE = Any

MAX_CONTENT_LENGTH = 10_000_000


class StarletteUserProxy(A2AUser):
"""Adapts the Starlette User class to the A2A user representation."""
Expand Down Expand Up @@ -134,7 +132,7 @@
"""
user: A2AUser = UnauthenticatedUser()
state = {}
with contextlib.suppress(Exception):
with contextlib.suppress(AttributeError):
user = StarletteUserProxy(request.user)
state['auth'] = request.auth
state['headers'] = dict(request.headers)
Expand Down Expand Up @@ -174,35 +172,38 @@
for model in A2ARequestModel.__args__
}

def __init__( # noqa: PLR0913
self,
agent_card: AgentCard,
http_handler: RequestHandler,
extended_agent_card: AgentCard | None = None,
context_builder: CallContextBuilder | None = None,
card_modifier: Callable[[AgentCard], AgentCard] | None = None,
extended_card_modifier: Callable[
[AgentCard, ServerCallContext], AgentCard
]
| None = None,
max_content_length: int | None = 10 * 1024 * 1024, # 10MB
) -> None:
"""Initializes the JSONRPCApplication.

Args:
agent_card: The AgentCard describing the agent's capabilities.
http_handler: The handler instance responsible for processing A2A
requests via http.
extended_agent_card: An optional, distinct AgentCard to be served
at the authenticated extended card endpoint.
context_builder: The CallContextBuilder used to construct the
ServerCallContext passed to the http_handler. If None, no
ServerCallContext is passed.
card_modifier: An optional callback to dynamically modify the public
agent card before it is served.
extended_card_modifier: An optional callback to dynamically modify
the extended agent card before it is served. It receives the
call context.
max_content_length: The maximum allowed content length for incoming
requests. Defaults to 10MB. Set to None for unbounded maximum.
"""

Check notice on line 206 in src/a2a/server/apps/jsonrpc/jsonrpc_app.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Copy/pasted code

see src/a2a/server/apps/rest/rest_adapter.py (55-82)
if not _package_starlette_installed:
raise ImportError(
'Packages `starlette` and `sse-starlette` are required to use the'
Expand All @@ -220,6 +221,7 @@
extended_card_modifier=extended_card_modifier,
)
self._context_builder = context_builder or DefaultCallContextBuilder()
self._max_content_length = max_content_length

def _generate_error_response(
self, request_id: str | int | None, error: JSONRPCError | A2AError
Expand Down Expand Up @@ -261,6 +263,22 @@
status_code=200,
)

def _allowed_content_length(self, request: Request) -> bool:
"""Checks if the request content length is within the allowed maximum.

Args:
request: The incoming Starlette Request object.

Returns:
False if the content length is larger than the allowed maximum, True otherwise.
"""
if self._max_content_length is not None:
with contextlib.suppress(ValueError):
content_length = int(request.headers.get('content-length', '0'))
if content_length and content_length > self._max_content_length:
return False
return True

async def _handle_requests(self, request: Request) -> Response: # noqa: PLR0911
"""Handles incoming POST requests to the main A2A endpoint.

Expand Down Expand Up @@ -291,18 +309,14 @@
request_id, str | int
):
request_id = None
# Treat very large payloads as invalid request (-32600) before routing
with contextlib.suppress(Exception):
content_length = int(request.headers.get('content-length', '0'))
if content_length and content_length > MAX_CONTENT_LENGTH:
return self._generate_error_response(
request_id,
A2AError(
root=InvalidRequestError(
message='Payload too large'
)
),
)
# Treat payloads lager than allowed as invalid request (-32600) before routing
if not self._allowed_content_length(request):
return self._generate_error_response(
request_id,
A2AError(
root=InvalidRequestError(message='Payload too large')
),
)
logger.debug('Request body: %s', body)
# 1) Validate base JSON-RPC structure only (-32600 on failure)
try:
Expand Down
4 changes: 4 additions & 0 deletions src/a2a/server/apps/jsonrpc/starlette_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,18 @@
(SSE).
"""

def __init__( # noqa: PLR0913
self,
agent_card: AgentCard,
http_handler: RequestHandler,
extended_agent_card: AgentCard | None = None,
context_builder: CallContextBuilder | None = None,
card_modifier: Callable[[AgentCard], AgentCard] | None = None,
extended_card_modifier: Callable[
[AgentCard, ServerCallContext], AgentCard
]
| None = None,
max_content_length: int | None = 10 * 1024 * 1024, # 10MB

Check notice on line 62 in src/a2a/server/apps/jsonrpc/starlette_app.py

View workflow job for this annotation

GitHub Actions / Lint Code Base

Copy/pasted code

see src/a2a/server/apps/rest/rest_adapter.py (55-66)
) -> None:
"""Initializes the A2AStarletteApplication.

Expand All @@ -76,6 +77,8 @@
extended_card_modifier: An optional callback to dynamically modify
the extended agent card before it is served. It receives the
call context.
max_content_length: The maximum allowed content length for incoming
requests. Defaults to 10MB. Set to None for unbounded maximum.
"""
if not _package_starlette_installed:
raise ImportError(
Expand All @@ -90,6 +93,7 @@
context_builder=context_builder,
card_modifier=card_modifier,
extended_card_modifier=extended_card_modifier,
max_content_length=max_content_length,
)

def routes(
Expand Down
36 changes: 36 additions & 0 deletions tests/server/apps/jsonrpc/test_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,42 @@ def test_handle_oversized_payload(agent_card_with_api_key: AgentCard):
assert data['error']['code'] == InvalidRequestError().code


@pytest.mark.parametrize(
'max_content_length',
[
None,
11 * 1024 * 1024,
30 * 1024 * 1024,
],
)
def test_handle_oversized_payload_with_max_content_length(
agent_card_with_api_key: AgentCard,
max_content_length: int | None,
):
"""Test handling of JSON payloads with sizes within custom max_content_length."""
handler = mock.AsyncMock()
app_instance = A2AStarletteApplication(
agent_card_with_api_key, handler, max_content_length=max_content_length
)
client = TestClient(app_instance.build())

large_string = 'a' * 11 * 1_000_000 # 11MB string
payload = {
'jsonrpc': '2.0',
'method': 'test',
'id': 1,
'params': {'data': large_string},
}

response = client.post('/', json=payload)
assert response.status_code == 200
data = response.json()
# When max_content_length is set, requests up to that size should not be
# rejected due to payload size. The request might fail for other reasons,
# but it shouldn't be an InvalidRequestError related to the content length.
assert data['error']['code'] != InvalidRequestError().code


def test_handle_unicode_characters(agent_card_with_api_key: AgentCard):
"""Test handling of unicode characters in JSON payload."""
handler = mock.AsyncMock()
Expand Down
Loading