From d06b96c78c2c19ccbee2a871dd2bee4428a36fb9 Mon Sep 17 00:00:00 2001 From: Iwaniukooo11 Date: Mon, 2 Nov 2026 16:06:37 +0000 Subject: [PATCH] refactor: remove unnecesary httpx usage in http_helpers --- src/a2a/client/transports/http_helpers.py | 11 +++-- tests/client/transports/test_http_helpers.py | 49 +++++++++++++++++++ .../client/transports/test_jsonrpc_client.py | 2 +- tests/client/transports/test_rest_client.py | 2 +- 4 files changed, 58 insertions(+), 6 deletions(-) diff --git a/src/a2a/client/transports/http_helpers.py b/src/a2a/client/transports/http_helpers.py index b0d601321..bda7bd29e 100644 --- a/src/a2a/client/transports/http_helpers.py +++ b/src/a2a/client/transports/http_helpers.py @@ -46,7 +46,7 @@ def get_http_args(context: ClientCallContext | None) -> dict[str, Any]: if context and context.service_parameters: http_kwargs['headers'] = context.service_parameters.copy() if context and context.timeout is not None: - http_kwargs['timeout'] = httpx.Timeout(context.timeout) + http_kwargs['timeout'] = context.timeout return http_kwargs @@ -179,9 +179,12 @@ def __init__( url: str, **kwargs: Any, ) -> None: - headers = httpx.Headers(kwargs.pop('headers', None)) - headers.setdefault('Accept', 'text/event-stream') - headers.setdefault('Cache-Control', 'no-store') + headers: dict[str, str] = dict(kwargs.pop('headers', None) or {}) + present = {key.lower() for key in headers} + if 'accept' not in present: + headers['Accept'] = 'text/event-stream' + if 'cache-control' not in present: + headers['Cache-Control'] = 'no-store' self._request = client.build_request( method, url, headers=headers, **kwargs ) diff --git a/tests/client/transports/test_http_helpers.py b/tests/client/transports/test_http_helpers.py index 8ef5d2d89..ddda42adb 100644 --- a/tests/client/transports/test_http_helpers.py +++ b/tests/client/transports/test_http_helpers.py @@ -1,9 +1,12 @@ import httpx import pytest +from a2a.client.client import ClientCallContext from a2a.client.errors import A2AClientError from a2a.client.transports.http_helpers import ( + _SSEEventSource, _default_sse_error_handler, + get_http_args, parse_sse_stream, send_http_stream_request, ) @@ -62,3 +65,49 @@ async def test_send_http_stream_request_non_sse(mocker): c async for c in send_http_stream_request(client, 'GET', 'http://test') ] assert chunks == ['plain error response'] + + +def test_get_http_args_passes_timeout_through_unwrapped(): + """The timeout stays a bare float, for the client itself to wrap.""" + http_kwargs = get_http_args(ClientCallContext(timeout=5.0)) + + assert http_kwargs['timeout'] == 5.0 + assert not isinstance(http_kwargs['timeout'], httpx.Timeout) + + request = httpx.AsyncClient().build_request( + 'GET', 'http://test', **http_kwargs + ) + assert request.extensions['timeout'] == { + 'connect': 5.0, + 'read': 5.0, + 'write': 5.0, + 'pool': 5.0, + } + + +def test_sse_event_source_sets_default_stream_headers(): + source = _SSEEventSource(httpx.AsyncClient(), 'GET', 'http://test') + + assert source._request.headers['Accept'] == 'text/event-stream' + assert source._request.headers['Cache-Control'] == 'no-store' + + +@pytest.mark.parametrize( + 'caller_spelling', ['accept', 'Accept', 'ACCEPT', 'AcCePt'] +) +def test_sse_event_source_keeps_caller_accept_whatever_its_case( + caller_spelling: str, +): + """A caller's own Accept wins, and is never joined by a second one. + + HTTP header names are case-insensitive, so matching the caller's + spelling exactly is not enough to detect that they already set one. + """ + source = _SSEEventSource( + httpx.AsyncClient(), + 'GET', + 'http://test', + headers={caller_spelling: 'application/json'}, + ) + + assert source._request.headers.get_list('accept') == ['application/json'] diff --git a/tests/client/transports/test_jsonrpc_client.py b/tests/client/transports/test_jsonrpc_client.py index aaeb1e8c8..493d99d48 100644 --- a/tests/client/transports/test_jsonrpc_client.py +++ b/tests/client/transports/test_jsonrpc_client.py @@ -245,7 +245,7 @@ async def test_send_message_with_timeout_context( mock_httpx_client.build_request.assert_called_once() _, kwargs = mock_httpx_client.build_request.call_args assert 'timeout' in kwargs - assert kwargs['timeout'] == httpx.Timeout(15.0) + assert kwargs['timeout'] == 15.0 class TestGetTask: diff --git a/tests/client/transports/test_rest_client.py b/tests/client/transports/test_rest_client.py index 3c0a7da29..558a21a8c 100644 --- a/tests/client/transports/test_rest_client.py +++ b/tests/client/transports/test_rest_client.py @@ -220,7 +220,7 @@ async def test_send_message_with_timeout_context( mock_build_request.assert_called_once() _, kwargs = mock_build_request.call_args assert 'timeout' in kwargs - assert kwargs['timeout'] == httpx.Timeout(10.0) + assert kwargs['timeout'] == 10.0 @pytest.mark.asyncio async def test_url_serialization(