diff --git a/src/a2a/client/client_factory.py b/src/a2a/client/client_factory.py index a59189ade..75e4fe932 100644 --- a/src/a2a/client/client_factory.py +++ b/src/a2a/client/client_factory.py @@ -78,7 +78,13 @@ def __init__( config: ClientConfig | None = None, ): config = config or ClientConfig() - httpx_client = config.httpx_client or httpx.AsyncClient() + # Follow redirects by default: a server may 307 a trailing-slash + # variant of the JSON-RPC/REST endpoint (Starlette's default + # redirect_slashes). 307 preserves method and body, so this is safe + # for POST payloads and keeps the client robust to either spelling. + httpx_client = config.httpx_client or httpx.AsyncClient( + follow_redirects=True + ) httpx_client.headers.setdefault( VERSION_HEADER, PROTOCOL_VERSION_CURRENT ) diff --git a/src/a2a/server/routes/jsonrpc_routes.py b/src/a2a/server/routes/jsonrpc_routes.py index a94d513ae..5b44d7f76 100644 --- a/src/a2a/server/routes/jsonrpc_routes.py +++ b/src/a2a/server/routes/jsonrpc_routes.py @@ -64,5 +64,5 @@ def create_jsonrpc_routes( path=rpc_url, endpoint=dispatcher.handle_requests, methods=['POST'], - ) + ), ] diff --git a/tck/sut_agent.py b/tck/sut_agent.py index 0ca3a1450..206774c64 100644 --- a/tck/sut_agent.py +++ b/tck/sut_agent.py @@ -14,6 +14,7 @@ import a2a.types.a2a_pb2_grpc as a2a_grpc from a2a.compat.v0_3.grpc_handler import CompatGrpcHandler +from a2a.helpers.proto_helpers import new_task_from_user_message from a2a.server.agent_execution.agent_executor import AgentExecutor from a2a.server.agent_execution.context import RequestContext from a2a.server.events.event_queue import EventQueue @@ -87,6 +88,13 @@ async def execute( self.running_tasks.add(task_id) + # 1.0 semantics: the Task itself must be enqueued before any + # TaskStatusUpdateEvent (the SDK enforces this ordering). + task = context.current_task + if not task: + task = new_task_from_user_message(user_message) + await event_queue.enqueue_event(task) + logger.info( '[SUTAgentExecutor] Processing message %s for task %s (context: %s)', user_message.message_id, @@ -157,11 +165,11 @@ def serve(task_store: TaskStore) -> None: ), AgentInterface( url=f'http://localhost:{http_port}{REST_URL}', - protocol_binding='REST', + protocol_binding='HTTP+JSON', protocol_version='1.0.0', ), AgentInterface( - url=f'http://localhost:{grpc_port}', + url=f'localhost:{grpc_port}', protocol_binding='GRPC', protocol_version='1.0.0', ), diff --git a/tests/client/test_client_factory.py b/tests/client/test_client_factory.py index d211a7331..5683bcda3 100644 --- a/tests/client/test_client_factory.py +++ b/tests/client/test_client_factory.py @@ -136,6 +136,24 @@ def test_client_factory_create_with_default_config( assert client._transport.url == 'http://primary-url.com' # type: ignore[attr-defined] +def test_client_factory_default_client_follows_redirects( + base_agent_card: AgentCard, +): + """Default factory client follows redirects (307 trailing-slash handling).""" + factory = ClientFactory() + assert factory._httpx_client.follow_redirects is True + + +def test_client_factory_respects_custom_client_redirect_setting( + base_agent_card: AgentCard, +): + """A user-supplied httpx client keeps its own redirect policy.""" + custom = httpx.AsyncClient(follow_redirects=False) + factory = ClientFactory(ClientConfig(httpx_client=custom)) + assert factory._httpx_client is custom + assert factory._httpx_client.follow_redirects is False + + @pytest.mark.asyncio async def test_client_factory_create_from_url(base_agent_card: AgentCard): """Verify that create_from_url resolves the card and creates a client."""