fix: support complete agent card URL in A2ACardResolver by ehsavoie · Pull Request #837 · a2aproject/a2a-java · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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: 2 additions & 2 deletions README.md
7 changes: 6 additions & 1 deletion client/base/src/main/java/org/a2aproject/sdk/A2A.java
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,12 @@ public static AgentCard getAgentCard(String agentUrl, String relativeCardPath, M
* @throws org.a2aproject.sdk.spec.A2AClientJSONError if the response body cannot be decoded as JSON or validated against the AgentCard schema
*/
public static AgentCard getAgentCard(A2AHttpClient httpClient, String agentUrl, String relativeCardPath, Map<String, String> authHeaders) throws A2AClientError, A2AClientJSONError {
A2ACardResolver resolver = new A2ACardResolver(httpClient, agentUrl, "", relativeCardPath, authHeaders);
A2ACardResolver resolver = A2ACardResolver.builder()
.httpClient(httpClient)
.baseUrl(agentUrl)
.agentCardPath(relativeCardPath)
.authHeaders(authHeaders)
.build();
return resolver.getAgentCard();
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package org.a2aproject.sdk.compat03.client.http;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Map;

import static org.a2aproject.sdk.util.Assert.checkNotNullParam;

import org.a2aproject.sdk.client.http.A2AHttpClient;
import org.a2aproject.sdk.client.http.A2AHttpClientFactory;
import org.a2aproject.sdk.client.http.A2AHttpResponse;
Expand All @@ -13,15 +14,14 @@
import org.a2aproject.sdk.compat03.spec.A2AClientError_v0_3;
import org.a2aproject.sdk.compat03.spec.A2AClientJSONError_v0_3;
import org.a2aproject.sdk.compat03.spec.AgentCard_v0_3;
import org.a2aproject.sdk.util.Utils;
import org.jspecify.annotations.Nullable;

public class A2ACardResolver_v0_3 {
private final A2AHttpClient httpClient;
private final String url;
private final @Nullable Map<String, String> authHeaders;

private static final String DEFAULT_AGENT_CARD_PATH = "/.well-known/agent-card.json";

/**
* Get the agent card for an A2A agent.
* The HTTP client will be auto-selected via {@link A2AHttpClientFactory}.
Expand Down Expand Up @@ -65,14 +65,17 @@ public A2ACardResolver_v0_3(A2AHttpClient httpClient, String baseUrl, String age
*/
public A2ACardResolver_v0_3(A2AHttpClient httpClient, String baseUrl, @Nullable String agentCardPath,
@Nullable Map<String, String> authHeaders) throws A2AClientError_v0_3 {
checkNotNullParam("httpClient", httpClient);
checkNotNullParam("baseUrl", baseUrl);
this.httpClient = httpClient;
String effectiveAgentCardPath = agentCardPath == null || agentCardPath.isEmpty() ? DEFAULT_AGENT_CARD_PATH : agentCardPath;
String effectiveAgentCardPath = agentCardPath == null || agentCardPath.isEmpty() ? Utils.DEFAULT_AGENT_CARD_PATH : agentCardPath;
try {
this.url = new URI(baseUrl).resolve(effectiveAgentCardPath).toString();
Utils.validateAbsoluteUrl(baseUrl);
this.url = Utils.buildCardUrl(Utils.stripWellKnownSuffix(baseUrl), effectiveAgentCardPath);
} catch (URISyntaxException e) {
throw new A2AClientError_v0_3("Invalid agent URL", e);
}
this.authHeaders = authHeaders;
this.authHeaders = authHeaders != null ? Map.copyOf(authHeaders) : null;
}

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

if (authHeaders != null) {
for (Map.Entry<String, String> entry : authHeaders.entrySet()) {
builder.addHeader(entry.getKey(), entry.getValue());
}
builder.addHeaders(authHeaders);
}

String body;
Expand All @@ -100,7 +101,10 @@ public AgentCard_v0_3 getAgentCard() throws A2AClientError_v0_3, A2AClientJSONEr
throw new A2AClientError_v0_3("Failed to obtain agent card: " + response.status());
}
body = response.body();
} catch (IOException | InterruptedException e) {
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new A2AClientError_v0_3("Failed to obtain agent card", e);
} catch (IOException e) {
throw new A2AClientError_v0_3("Failed to obtain agent card", e);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,45 @@ public void testConstructorStripsSlashes() throws Exception {
card = resolver.getAgentCard();

assertEquals("http://example.com" + AGENT_CARD_PATH, client.url);

// baseUrl with sub-path and trailing slash — the original URI.resolve() bug silently
// dropped the sub-path, producing http://example.com/.well-known/agent-card.json instead
resolver = new A2ACardResolver_v0_3(client, "http://example.com/jsonrpc/", AGENT_CARD_PATH);
card = resolver.getAgentCard();

assertEquals("http://example.com/jsonrpc" + AGENT_CARD_PATH, client.url);

// baseUrl with sub-path, no trailing slash
resolver = new A2ACardResolver_v0_3(client, "http://example.com/jsonrpc", AGENT_CARD_PATH);
card = resolver.getAgentCard();

assertEquals("http://example.com/jsonrpc" + AGENT_CARD_PATH, client.url);
}


@Test
public void testBaseUrl_alreadyContainsWellKnownPath() throws Exception {
TestHttpClient client = new TestHttpClient();
client.body = JsonMessages_v0_3.AGENT_CARD;

String fullUrl = "https://example.com/spec03" + AGENT_CARD_PATH;
A2ACardResolver_v0_3 resolver = new A2ACardResolver_v0_3(client, fullUrl);
resolver.getAgentCard();

assertEquals(fullUrl, client.url);
}

@Test
public void testFullWellKnownUrlWithCustomAgentCardPath() throws Exception {
TestHttpClient client = new TestHttpClient();
client.body = JsonMessages_v0_3.AGENT_CARD;

A2ACardResolver_v0_3 resolver = new A2ACardResolver_v0_3(
client, "https://example.com/spec03" + AGENT_CARD_PATH, "/custom/card.json");
resolver.getAgentCard();

assertEquals("https://example.com/spec03/custom/card.json", client.url);
}

@Test
public void testGetAgentCardSuccess() throws Exception {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public class HelloWorldClient {
public static void main(String[] args) {
OpenTelemetrySdk openTelemetrySdk = null;
try {
AgentCard publicAgentCard = new A2ACardResolver(SERVER_URL).getAgentCard();
AgentCard publicAgentCard = A2ACardResolver.builder().baseUrl(SERVER_URL).build().getAgentCard();
System.out.println("Successfully fetched public agent card:");
System.out.println(JsonUtil.toJson(publicAgentCard));
System.out.println("Using public agent card for client initialization (default).");
Expand Down
4 changes: 2 additions & 2 deletions extras/http-client-vertx/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ The Vert.x HTTP client is automatically discovered via **Java SPI (Service Provi

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

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

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

// Example 2: Using REST transport (uses HTTP client internally)
Expand Down
5 changes: 5 additions & 0 deletions http-client/pom.xml
Loading