Skip to content
Open
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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,23 @@ Client client = Client
.build();
```

To customize the default JDK HTTP client without replacing the SDK implementation, provide
your own `java.net.http.HttpClient` to `JdkA2AHttpClient`:

```java
HttpClient jdkHttpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.followRedirects(HttpClient.Redirect.NORMAL)
.version(HttpClient.Version.HTTP_2)
.build();

Client client = Client
.builder(agentCard)
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig(
new JdkA2AHttpClient(jdkHttpClient)))
.build();
```

##### gRPC Transport Configuration

For the gRPC transport, you must configure a channel factory:
Expand Down
15 changes: 13 additions & 2 deletions http-client/src/main/java/io/a2a/client/http/JdkA2AHttpClient.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.a2a.client.http;

import static io.a2a.util.Assert.checkNotNullParam;
import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
import static java.net.HttpURLConnection.HTTP_MULT_CHOICE;
import static java.net.HttpURLConnection.HTTP_OK;
Expand Down Expand Up @@ -61,10 +62,20 @@ public class JdkA2AHttpClient implements A2AHttpClient {
* </ul>
*/
public JdkA2AHttpClient() {
httpClient = HttpClient.newBuilder()
this(HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.followRedirects(HttpClient.Redirect.NORMAL)
.build();
.build());
}

/**
* Creates a new JDK-based HTTP client using a caller-provided JDK {@link HttpClient}.
*
* @param httpClient the JDK HTTP client to delegate requests to
* @throws IllegalArgumentException if {@code httpClient} is {@code null}
*/
public JdkA2AHttpClient(HttpClient httpClient) {
this.httpClient = checkNotNullParam("httpClient", httpClient);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package io.a2a.client.http;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockserver.integration.ClientAndServer;

import java.io.IOException;
import java.net.Proxy;
import java.net.ProxySelector;
import java.net.SocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockserver.model.HttpRequest.request;
import static org.mockserver.model.HttpResponse.response;

public class JdkA2AHttpClientTest {

private ClientAndServer server;

@AfterEach
public void tearDown() {
if (server != null) {
server.stop();
}
}

@Test
public void testDefaultConstructorCreatesUsableClient() throws Exception {
server = ClientAndServer.startClientAndServer(0);
server.when(request().withMethod("GET").withPath("/default"))
.respond(response().withStatusCode(200).withBody("ok"));

JdkA2AHttpClient client = new JdkA2AHttpClient();

A2AHttpResponse response = client.createGet()
.url("http://localhost:" + server.getLocalPort() + "/default")
.get();

assertEquals(200, response.status());
assertEquals("ok", response.body());
}

@Test
public void testConstructorUsesProvidedHttpClient() throws Exception {
server = ClientAndServer.startClientAndServer(0);
server.when(request().withMethod("GET").withPath("/custom"))
.respond(response().withStatusCode(200).withBody("ok"));

TrackingProxySelector proxySelector = new TrackingProxySelector();
HttpClient providedClient = HttpClient.newBuilder()
.proxy(proxySelector)
.build();

JdkA2AHttpClient client = new JdkA2AHttpClient(providedClient);

A2AHttpResponse response = client.createGet()
.url("http://localhost:" + server.getLocalPort() + "/custom")
.get();

assertEquals(200, response.status());
assertEquals("ok", response.body());
assertEquals(1, proxySelector.selectCount.get(),
"Provided HttpClient should be used for request execution");
}

@Test
public void testConstructorRejectsNullHttpClient() {
assertThrows(IllegalArgumentException.class, () -> new JdkA2AHttpClient(null), "foo");
}

private static final class TrackingProxySelector extends ProxySelector {
private final AtomicInteger selectCount = new AtomicInteger();

@Override
public List<Proxy> select(URI uri) {
selectCount.incrementAndGet();
return List.of(Proxy.NO_PROXY);
}

@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
throw new AssertionError("Proxy connection should not fail in this test", ioe);
}
}
}
Loading