diff --git a/src/strands/multiagent/a2a/server.py b/src/strands/multiagent/a2a/server.py index 7b4c4c73a..fd90e9787 100644 --- a/src/strands/multiagent/a2a/server.py +++ b/src/strands/multiagent/a2a/server.py @@ -79,6 +79,7 @@ def __init__( # Parse the provided URL to extract components for mounting self.public_base_url, self.mount_path = self._parse_public_url(http_url) self.http_url = http_url.rstrip("/") + "/" + self._http_url_explicit = True # Override mount path if serve_at_root is requested if serve_at_root: @@ -88,6 +89,7 @@ def __init__( self.public_base_url = f"http://{host}:{port}" self.http_url = f"{self.public_base_url}/" self.mount_path = "" + self._http_url_explicit = False self.strands_agent = agent self.name = self.strands_agent.name @@ -253,12 +255,25 @@ def serve( port: The port number to bind the server to. Defaults to 9000. **kwargs: Additional keyword arguments to pass to uvicorn.run. """ + # Update host/port if overridden, and recalculate URLs if http_url wasn't explicitly set + if host is not None: + self.host = host + if port is not None: + self.port = port + + if host is not None or port is not None: + # Only update the URL if it wasn't explicitly set via http_url parameter + # (i.e., if the URL was auto-generated from host/port in __init__) + if not self._http_url_explicit: + self.public_base_url = f"http://{self.host}:{self.port}" + self.http_url = f"{self.public_base_url}/" + try: logger.info("Starting Strands A2A server...") if app_type == "fastapi": - uvicorn.run(self.to_fastapi_app(), host=host or self.host, port=port or self.port, **kwargs) + uvicorn.run(self.to_fastapi_app(), host=self.host, port=self.port, **kwargs) else: - uvicorn.run(self.to_starlette_app(), host=host or self.host, port=port or self.port, **kwargs) + uvicorn.run(self.to_starlette_app(), host=self.host, port=self.port, **kwargs) except KeyboardInterrupt: logger.warning("Strands A2A server shutdown requested (KeyboardInterrupt).") except Exception: diff --git a/tests/strands/multiagent/a2a/test_server.py b/tests/strands/multiagent/a2a/test_server.py index 647fce230..aeb882b19 100644 --- a/tests/strands/multiagent/a2a/test_server.py +++ b/tests/strands/multiagent/a2a/test_server.py @@ -876,3 +876,150 @@ def test_to_fastapi_app_with_app_kwargs(mock_strands_agent): assert isinstance(app, FastAPI) assert app.title == "Custom Agent Title" + + +@patch("uvicorn.run") +def test_serve_with_overridden_host_port_updates_agent_card_url(mock_run, mock_strands_agent): + """Test that serve() with host/port overrides updates the agent card URL. + + This test verifies the fix for issue #1258 where specifying host/port in serve() + did not update the agent card URL, causing clients to fail when trying to connect. + """ + mock_strands_agent.tool_registry.get_all_tools_config.return_value = {} + + a2a_agent = A2AServer(mock_strands_agent, skills=[]) + + # Verify initial URL from constructor defaults + assert a2a_agent.http_url == "http://127.0.0.1:9000/" + assert a2a_agent.public_base_url == "http://127.0.0.1:9000" + + # Call serve with different host and port + a2a_agent.serve(host="localhost", port=9210) + + # Verify URL was updated to match the actual serve parameters + assert a2a_agent.http_url == "http://localhost:9210/" + assert a2a_agent.public_base_url == "http://localhost:9210" + assert a2a_agent.host == "localhost" + assert a2a_agent.port == 9210 + + # Verify the agent card reflects the updated URL + card = a2a_agent.public_agent_card + assert card.url == "http://localhost:9210/" + + # Verify uvicorn was called with the overridden parameters + mock_run.assert_called_once() + _, kwargs = mock_run.call_args + assert kwargs["host"] == "localhost" + assert kwargs["port"] == 9210 + + +@patch("uvicorn.run") +def test_serve_with_overridden_port_only_updates_url(mock_run, mock_strands_agent): + """Test that serve() with only port override updates the agent card URL.""" + mock_strands_agent.tool_registry.get_all_tools_config.return_value = {} + + a2a_agent = A2AServer(mock_strands_agent, skills=[]) + + # Call serve with different port only + a2a_agent.serve(port=8080) + + # Verify URL was updated with the new port + assert a2a_agent.http_url == "http://127.0.0.1:8080/" + assert a2a_agent.port == 8080 + + # Verify uvicorn was called with the correct parameters + mock_run.assert_called_once() + _, kwargs = mock_run.call_args + assert kwargs["host"] == "127.0.0.1" + assert kwargs["port"] == 8080 + + +@patch("uvicorn.run") +def test_serve_with_overridden_host_only_updates_url(mock_run, mock_strands_agent): + """Test that serve() with only host override updates the agent card URL.""" + mock_strands_agent.tool_registry.get_all_tools_config.return_value = {} + + a2a_agent = A2AServer(mock_strands_agent, skills=[]) + + # Call serve with different host only + a2a_agent.serve(host="0.0.0.0") + + # Verify URL was updated with the new host + assert a2a_agent.http_url == "http://0.0.0.0:9000/" + assert a2a_agent.host == "0.0.0.0" + + # Verify uvicorn was called with the correct parameters + mock_run.assert_called_once() + _, kwargs = mock_run.call_args + assert kwargs["host"] == "0.0.0.0" + assert kwargs["port"] == 9000 + + +@patch("uvicorn.run") +def test_serve_with_explicit_http_url_does_not_override_url(mock_run, mock_strands_agent): + """Test that serve() with host/port does not override explicitly set http_url. + + When a user explicitly sets http_url in the constructor (e.g., for load balancer scenarios), + the serve() method should NOT override the URL even if host/port are provided. + """ + mock_strands_agent.tool_registry.get_all_tools_config.return_value = {} + + # Create server with explicit http_url (simulating load balancer scenario) + a2a_agent = A2AServer( + mock_strands_agent, + host="0.0.0.0", + port=8080, + http_url="https://my-alb.amazonaws.com/agent1", + skills=[], + ) + + # Verify initial URL is the explicit one + assert a2a_agent.http_url == "https://my-alb.amazonaws.com/agent1/" + assert a2a_agent._http_url_explicit is True + + # Call serve with different host/port (the local binding) + a2a_agent.serve(host="0.0.0.0", port=9000) + + # Verify URL was NOT changed (explicit http_url should be preserved) + assert a2a_agent.http_url == "https://my-alb.amazonaws.com/agent1/" + assert a2a_agent.public_base_url == "https://my-alb.amazonaws.com" + + # But host/port should still be updated for the actual binding + assert a2a_agent.host == "0.0.0.0" + assert a2a_agent.port == 9000 + + # Verify the agent card still shows the public URL + card = a2a_agent.public_agent_card + assert card.url == "https://my-alb.amazonaws.com/agent1/" + + +@patch("uvicorn.run") +def test_serve_without_overrides_does_not_change_url(mock_run, mock_strands_agent): + """Test that serve() without host/port parameters does not modify the URL.""" + mock_strands_agent.tool_registry.get_all_tools_config.return_value = {} + + a2a_agent = A2AServer(mock_strands_agent, host="localhost", port=8000, skills=[]) + + # Verify initial URL + assert a2a_agent.http_url == "http://localhost:8000/" + + # Call serve without overrides + a2a_agent.serve() + + # Verify URL was NOT changed + assert a2a_agent.http_url == "http://localhost:8000/" + assert a2a_agent.host == "localhost" + assert a2a_agent.port == 8000 + + +def test_http_url_explicit_flag_set_correctly(mock_strands_agent): + """Test that _http_url_explicit flag is set correctly during initialization.""" + mock_strands_agent.tool_registry.get_all_tools_config.return_value = {} + + # Without explicit http_url + server1 = A2AServer(mock_strands_agent, skills=[]) + assert server1._http_url_explicit is False + + # With explicit http_url + server2 = A2AServer(mock_strands_agent, http_url="http://example.com/agent", skills=[]) + assert server2._http_url_explicit is True