Skip to content

Commit 3ced57f

Browse files
authored
fix: support complete agent card URL in A2ACardResolver (#837)
feat!: support complete agent card URL in A2ACardResolver URI.resolve() silently drops intermediate path segments when the resolved path starts with '/', producing wrong URLs for agents under a path prefix. Replace with direct concatenation and add a guard when the supplied URL already includes the agent card path. Signed-off-by: Emmanuel Hugonnet <ehugonne@redhat.com>
1 parent b2d78e4 commit 3ced57f

13 files changed

Lines changed: 709 additions & 197 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,7 @@ If you want to use the HTTP+JSON/REST transport, you'll need to add a relevant d
436436

437437
```java
438438
// First, get the agent card for the A2A server agent you want to connect to
439-
AgentCard agentCard = new A2ACardResolver("http://localhost:1234").getAgentCard();
439+
AgentCard agentCard = A2ACardResolver.builder().baseUrl("http://localhost:1234").build().getAgentCard();
440440

441441
// Specify configuration for the ClientBuilder
442442
ClientConfig clientConfig = new ClientConfig.Builder()
@@ -776,7 +776,7 @@ gRPC and REST transports are also available:
776776
#### 3. Create the v0.3 client
777777

778778
```java
779-
AgentCard card = new A2ACardResolver("http://localhost:1234").getAgentCard();
779+
AgentCard card = A2ACardResolver.builder().baseUrl("http://localhost:1234").build().getAgentCard();
780780

781781
// Find the v0.3 interface from the agent card
782782
AgentInterface v03Interface = card.supportedInterfaces().stream()

client/base/src/main/java/org/a2aproject/sdk/A2A.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,12 @@ public static AgentCard getAgentCard(String agentUrl, String relativeCardPath, M
393393
* @throws org.a2aproject.sdk.spec.A2AClientJSONError if the response body cannot be decoded as JSON or validated against the AgentCard schema
394394
*/
395395
public static AgentCard getAgentCard(A2AHttpClient httpClient, String agentUrl, String relativeCardPath, Map<String, String> authHeaders) throws A2AClientError, A2AClientJSONError {
396-
A2ACardResolver resolver = new A2ACardResolver(httpClient, agentUrl, "", relativeCardPath, authHeaders);
396+
A2ACardResolver resolver = A2ACardResolver.builder()
397+
.httpClient(httpClient)
398+
.baseUrl(agentUrl)
399+
.agentCardPath(relativeCardPath)
400+
.authHeaders(authHeaders)
401+
.build();
397402
return resolver.getAgentCard();
398403
}
399404
}

compat-0.3/client/transport/spi/src/main/java/org/a2aproject/sdk/compat03/client/http/A2ACardResolver_v0_3.java

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package org.a2aproject.sdk.compat03.client.http;
22

33
import java.io.IOException;
4-
import java.net.URI;
54
import java.net.URISyntaxException;
65
import java.util.Map;
76

7+
import static org.a2aproject.sdk.util.Assert.checkNotNullParam;
8+
89
import org.a2aproject.sdk.client.http.A2AHttpClient;
910
import org.a2aproject.sdk.client.http.A2AHttpClientFactory;
1011
import org.a2aproject.sdk.client.http.A2AHttpResponse;
@@ -13,15 +14,14 @@
1314
import org.a2aproject.sdk.compat03.spec.A2AClientError_v0_3;
1415
import org.a2aproject.sdk.compat03.spec.A2AClientJSONError_v0_3;
1516
import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3;
17+
import org.a2aproject.sdk.util.Utils;
1618
import org.jspecify.annotations.Nullable;
1719

1820
public class A2ACardResolver_v0_3 {
1921
private final A2AHttpClient httpClient;
2022
private final String url;
2123
private final @Nullable Map<String, String> authHeaders;
2224

23-
private static final String DEFAULT_AGENT_CARD_PATH = "/.well-known/agent-card.json";
24-
2525
/**
2626
* Get the agent card for an A2A agent.
2727
* The HTTP client will be auto-selected via {@link A2AHttpClientFactory}.
@@ -65,14 +65,17 @@ public A2ACardResolver_v0_3(A2AHttpClient httpClient, String baseUrl, String age
6565
*/
6666
public A2ACardResolver_v0_3(A2AHttpClient httpClient, String baseUrl, @Nullable String agentCardPath,
6767
@Nullable Map<String, String> authHeaders) throws A2AClientError_v0_3 {
68+
checkNotNullParam("httpClient", httpClient);
69+
checkNotNullParam("baseUrl", baseUrl);
6870
this.httpClient = httpClient;
69-
String effectiveAgentCardPath = agentCardPath == null || agentCardPath.isEmpty() ? DEFAULT_AGENT_CARD_PATH : agentCardPath;
71+
String effectiveAgentCardPath = agentCardPath == null || agentCardPath.isEmpty() ? Utils.DEFAULT_AGENT_CARD_PATH : agentCardPath;
7072
try {
71-
this.url = new URI(baseUrl).resolve(effectiveAgentCardPath).toString();
73+
Utils.validateAbsoluteUrl(baseUrl);
74+
this.url = Utils.buildCardUrl(Utils.stripWellKnownSuffix(baseUrl), effectiveAgentCardPath);
7275
} catch (URISyntaxException e) {
7376
throw new A2AClientError_v0_3("Invalid agent URL", e);
7477
}
75-
this.authHeaders = authHeaders;
78+
this.authHeaders = authHeaders != null ? Map.copyOf(authHeaders) : null;
7679
}
7780

7881
/**
@@ -88,9 +91,7 @@ public AgentCard_v0_3 getAgentCard() throws A2AClientError_v0_3, A2AClientJSONEr
8891
.addHeader("Content-Type", "application/json");
8992

9093
if (authHeaders != null) {
91-
for (Map.Entry<String, String> entry : authHeaders.entrySet()) {
92-
builder.addHeader(entry.getKey(), entry.getValue());
93-
}
94+
builder.addHeaders(authHeaders);
9495
}
9596

9697
String body;
@@ -100,7 +101,10 @@ public AgentCard_v0_3 getAgentCard() throws A2AClientError_v0_3, A2AClientJSONEr
100101
throw new A2AClientError_v0_3("Failed to obtain agent card: " + response.status());
101102
}
102103
body = response.body();
103-
} catch (IOException | InterruptedException e) {
104+
} catch (InterruptedException e) {
105+
Thread.currentThread().interrupt();
106+
throw new A2AClientError_v0_3("Failed to obtain agent card", e);
107+
} catch (IOException e) {
104108
throw new A2AClientError_v0_3("Failed to obtain agent card", e);
105109
}
106110

compat-0.3/client/transport/spi/src/test/java/org/a2aproject/sdk/compat03/client/http/A2ACardResolver_v0_3_Test.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,45 @@ public void testConstructorStripsSlashes() throws Exception {
6262
card = resolver.getAgentCard();
6363

6464
assertEquals("http://example.com" + AGENT_CARD_PATH, client.url);
65+
66+
// baseUrl with sub-path and trailing slash — the original URI.resolve() bug silently
67+
// dropped the sub-path, producing http://example.com/.well-known/agent-card.json instead
68+
resolver = new A2ACardResolver_v0_3(client, "http://example.com/jsonrpc/", AGENT_CARD_PATH);
69+
card = resolver.getAgentCard();
70+
71+
assertEquals("http://example.com/jsonrpc" + AGENT_CARD_PATH, client.url);
72+
73+
// baseUrl with sub-path, no trailing slash
74+
resolver = new A2ACardResolver_v0_3(client, "http://example.com/jsonrpc", AGENT_CARD_PATH);
75+
card = resolver.getAgentCard();
76+
77+
assertEquals("http://example.com/jsonrpc" + AGENT_CARD_PATH, client.url);
78+
}
79+
80+
81+
@Test
82+
public void testBaseUrl_alreadyContainsWellKnownPath() throws Exception {
83+
TestHttpClient client = new TestHttpClient();
84+
client.body = JsonMessages_v0_3.AGENT_CARD;
85+
86+
String fullUrl = "https://example.com/spec03" + AGENT_CARD_PATH;
87+
A2ACardResolver_v0_3 resolver = new A2ACardResolver_v0_3(client, fullUrl);
88+
resolver.getAgentCard();
89+
90+
assertEquals(fullUrl, client.url);
6591
}
6692

93+
@Test
94+
public void testFullWellKnownUrlWithCustomAgentCardPath() throws Exception {
95+
TestHttpClient client = new TestHttpClient();
96+
client.body = JsonMessages_v0_3.AGENT_CARD;
97+
98+
A2ACardResolver_v0_3 resolver = new A2ACardResolver_v0_3(
99+
client, "https://example.com/spec03" + AGENT_CARD_PATH, "/custom/card.json");
100+
resolver.getAgentCard();
101+
102+
assertEquals("https://example.com/spec03/custom/card.json", client.url);
103+
}
67104

68105
@Test
69106
public void testGetAgentCardSuccess() throws Exception {

examples/helloworld/client/src/main/java/org/a2aproject/sdk/examples/helloworld/HelloWorldClient.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public class HelloWorldClient {
5353
public static void main(String[] args) {
5454
OpenTelemetrySdk openTelemetrySdk = null;
5555
try {
56-
AgentCard publicAgentCard = new A2ACardResolver(SERVER_URL).getAgentCard();
56+
AgentCard publicAgentCard = A2ACardResolver.builder().baseUrl(SERVER_URL).build().getAgentCard();
5757
System.out.println("Successfully fetched public agent card:");
5858
System.out.println(JsonUtil.toJson(publicAgentCard));
5959
System.out.println("Using public agent card for client initialization (default).");

extras/http-client-vertx/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ The Vert.x HTTP client is automatically discovered via **Java SPI (Service Provi
8585

8686
```java
8787
// No changes needed - A2A SDK automatically uses VertxA2AHttpClient
88-
A2ACardResolver resolver = new A2ACardResolver("http://localhost:9999");
88+
A2ACardResolver resolver = A2ACardResolver.builder().baseUrl("http://localhost:9999").build();
8989
AgentCard card = resolver.getAgentCard(); // Uses Vert.x under the hood
9090

9191
// Client creation also uses Vert.x automatically
@@ -112,7 +112,7 @@ The module works out-of-the-box with sensible defaults:
112112
// With vertx-http-client on the classpath, it automatically uses VertxA2AHttpClient
113113

114114
// Example 1: Fetching agent card
115-
A2ACardResolver resolver = new A2ACardResolver("http://localhost:9999");
115+
A2ACardResolver resolver = A2ACardResolver.builder().baseUrl("http://localhost:9999").build();
116116
AgentCard card = resolver.getAgentCard();
117117

118118
// Example 2: Using REST transport (uses HTTP client internally)

http-client/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
3030
<artifactId>protobuf-java-util</artifactId>
3131
</dependency>
3232

33+
<dependency>
34+
<groupId>org.slf4j</groupId>
35+
<artifactId>slf4j-api</artifactId>
36+
</dependency>
37+
3338
<dependency>
3439
<groupId>org.junit.jupiter</groupId>
3540
<artifactId>junit-jupiter-api</artifactId>

0 commit comments

Comments
 (0)