Skip to content

Commit fd33b3d

Browse files
authored
feat(http-client): allow to construct a JdkA2AHttpClient with a prebuilt HttpClient (a2aproject#745)
# Description A prebuilt HttpClient can be used to adapt for environment specific circumstances, e.g. by configuring a proxy or a specific SSL context. - [x] Follow the [`CONTRIBUTING` Guide](../CONTRIBUTING.md). - [x] Make your Pull Request title in the <https://www.conventionalcommits.org/> specification. - Important Prefixes for [release-please](https://github.com/googleapis/release-please): - `fix:` which represents bug fixes, and correlates to a [SemVer](https://semver.org/) patch. - `feat:` represents a new feature, and correlates to a SemVer minor. - `feat!:`, or `fix!:`, `refactor!:`, etc., which represent a breaking change (indicated by the `!`) and will result in a SemVer major. - [x] Ensure the tests pass - [x] Appropriate READMEs were updated (if necessary) Fixes a2aproject#744 🦕
1 parent f81ffd1 commit fd33b3d

3 files changed

Lines changed: 121 additions & 2 deletions

File tree

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,23 @@ Client client = Client
395395
.build();
396396
```
397397

398+
To customize the default JDK HTTP client without replacing the SDK implementation, provide
399+
your own `java.net.http.HttpClient` to `JdkA2AHttpClient`:
400+
401+
```java
402+
HttpClient jdkHttpClient = HttpClient.newBuilder()
403+
.connectTimeout(Duration.ofSeconds(5))
404+
.followRedirects(HttpClient.Redirect.NORMAL)
405+
.version(HttpClient.Version.HTTP_2)
406+
.build();
407+
408+
Client client = Client
409+
.builder(agentCard)
410+
.withTransport(JSONRPCTransport.class, new JSONRPCTransportConfig(
411+
new JdkA2AHttpClient(jdkHttpClient)))
412+
.build();
413+
```
414+
398415
##### gRPC Transport Configuration
399416

400417
For the gRPC transport, you must configure a channel factory:

http-client/src/main/java/io/a2a/client/http/JdkA2AHttpClient.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.a2a.client.http;
22

3+
import static io.a2a.util.Assert.checkNotNullParam;
34
import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
45
import static java.net.HttpURLConnection.HTTP_MULT_CHOICE;
56
import static java.net.HttpURLConnection.HTTP_OK;
@@ -61,10 +62,20 @@ public class JdkA2AHttpClient implements A2AHttpClient {
6162
* </ul>
6263
*/
6364
public JdkA2AHttpClient() {
64-
httpClient = HttpClient.newBuilder()
65+
this(HttpClient.newBuilder()
6566
.version(HttpClient.Version.HTTP_2)
6667
.followRedirects(HttpClient.Redirect.NORMAL)
67-
.build();
68+
.build());
69+
}
70+
71+
/**
72+
* Creates a new JDK-based HTTP client using a caller-provided JDK {@link HttpClient}.
73+
*
74+
* @param httpClient the JDK HTTP client to delegate requests to
75+
* @throws IllegalArgumentException if {@code httpClient} is {@code null}
76+
*/
77+
public JdkA2AHttpClient(HttpClient httpClient) {
78+
this.httpClient = checkNotNullParam("httpClient", httpClient);
6879
}
6980

7081
@Override
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package io.a2a.client.http;
2+
3+
import org.junit.jupiter.api.AfterEach;
4+
import org.junit.jupiter.api.BeforeEach;
5+
import org.junit.jupiter.api.Test;
6+
import org.mockserver.integration.ClientAndServer;
7+
8+
import java.io.IOException;
9+
import java.net.Proxy;
10+
import java.net.ProxySelector;
11+
import java.net.SocketAddress;
12+
import java.net.URI;
13+
import java.net.http.HttpClient;
14+
import java.util.List;
15+
import java.util.concurrent.atomic.AtomicInteger;
16+
17+
import static org.junit.jupiter.api.Assertions.assertEquals;
18+
import static org.junit.jupiter.api.Assertions.assertThrows;
19+
import static org.mockserver.model.HttpRequest.request;
20+
import static org.mockserver.model.HttpResponse.response;
21+
22+
public class JdkA2AHttpClientTest {
23+
24+
private ClientAndServer server;
25+
26+
@AfterEach
27+
public void tearDown() {
28+
if (server != null) {
29+
server.stop();
30+
}
31+
}
32+
33+
@Test
34+
public void testDefaultConstructorCreatesUsableClient() throws Exception {
35+
server = ClientAndServer.startClientAndServer(0);
36+
server.when(request().withMethod("GET").withPath("/default"))
37+
.respond(response().withStatusCode(200).withBody("ok"));
38+
39+
JdkA2AHttpClient client = new JdkA2AHttpClient();
40+
41+
A2AHttpResponse response = client.createGet()
42+
.url("http://localhost:" + server.getLocalPort() + "/default")
43+
.get();
44+
45+
assertEquals(200, response.status());
46+
assertEquals("ok", response.body());
47+
}
48+
49+
@Test
50+
public void testConstructorUsesProvidedHttpClient() throws Exception {
51+
server = ClientAndServer.startClientAndServer(0);
52+
server.when(request().withMethod("GET").withPath("/custom"))
53+
.respond(response().withStatusCode(200).withBody("ok"));
54+
55+
TrackingProxySelector proxySelector = new TrackingProxySelector();
56+
HttpClient providedClient = HttpClient.newBuilder()
57+
.proxy(proxySelector)
58+
.build();
59+
60+
JdkA2AHttpClient client = new JdkA2AHttpClient(providedClient);
61+
62+
A2AHttpResponse response = client.createGet()
63+
.url("http://localhost:" + server.getLocalPort() + "/custom")
64+
.get();
65+
66+
assertEquals(200, response.status());
67+
assertEquals("ok", response.body());
68+
assertEquals(1, proxySelector.selectCount.get(),
69+
"Provided HttpClient should be used for request execution");
70+
}
71+
72+
@Test
73+
public void testConstructorRejectsNullHttpClient() {
74+
assertThrows(IllegalArgumentException.class, () -> new JdkA2AHttpClient(null), "foo");
75+
}
76+
77+
private static final class TrackingProxySelector extends ProxySelector {
78+
private final AtomicInteger selectCount = new AtomicInteger();
79+
80+
@Override
81+
public List<Proxy> select(URI uri) {
82+
selectCount.incrementAndGet();
83+
return List.of(Proxy.NO_PROXY);
84+
}
85+
86+
@Override
87+
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
88+
throw new AssertionError("Proxy connection should not fail in this test", ioe);
89+
}
90+
}
91+
}

0 commit comments

Comments
 (0)