Skip to content

Commit c2415a4

Browse files
paulrutterclaudekabir
authored
docs: document JDK HttpClient's restricted Host header and the Vert.x alternative (#1180)
* docs: document JDK HttpClient's restricted Host header and the Vert.x alternative java.net.http.HttpClient rejects an explicit "Host" header with an IllegalArgumentException unless the JVM is started with -Djdk.httpclient.allowRestrictedHeaders=host. This is a JDK-wide restriction, not something JdkA2AHttpClient can work around per-instance, and it wasn't documented anywhere, so callers who need a custom Host header (e.g. to reach an instance directly while presenting the hostname a load balancer would normally supply) had no clear path forward. Document the restriction and the two ways around it: setting the JVM property, or using VertxA2AHttpClient (from the http-client-vertx extra), which does not build on java.net.http.HttpClient and has no such restriction. Add regression tests that lock in both behaviors: the JDK client rejecting the header, and the Vert.x client accepting it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019bxqScUGdfjts6YHbLfUPs * fix: address review feedback on Host header docs and test - docs/content/dev/client.md: show VertxA2AHttpClient being closed in the Host-header example. The no-args constructor owns a Vert.x instance, and JSONRPCTransport.close() is a no-op that never closes an injected A2AHttpClient, so the previous example leaked it. - JdkA2AHttpClientTest: skip testCustomHostHeaderRejectedByJdkHttpClient when the JVM already allows a restricted Host header via -Djdk.httpclient.allowRestrictedHeaders=host, since the JDK client then legitimately accepts the header instead of rejecting it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019bxqScUGdfjts6YHbLfUPs * docs: close both clients in Host header example --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Kabir Khan <kkhan@redhat.com>
1 parent 7e73cab commit c2415a4

4 files changed

Lines changed: 93 additions & 0 deletions

File tree

‎docs/content/dev/client.md‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,34 @@ Client client = Client
151151
.build();
152152
```
153153

154+
### Setting a Custom `Host` Header
155+
156+
The JDK-based `JdkA2AHttpClient` (the default) delegates to `java.net.http.HttpClient`,
157+
which rejects an explicit `Host` header (`addHeader("Host", ...)`) with an
158+
`IllegalArgumentException` unless the JVM is started with
159+
`-Djdk.httpclient.allowRestrictedHeaders=host`. This is a JDK-wide restriction and
160+
cannot be worked around per-client-instance.
161+
162+
If you need to send a custom `Host` header — for example, connecting directly to an
163+
instance by IP while presenting the hostname a load balancer would normally supply —
164+
either set that JVM property, or use the `VertxA2AHttpClient` from the
165+
`a2a-java-sdk-http-client-vertx` extra, which does not build on `java.net.http.HttpClient`
166+
and has no such restriction:
167+
168+
```java
169+
// Both resources are closed separately because Client.close() does not close an injected
170+
// A2AHttpClient. VertxA2AHttpClient's no-args constructor also owns the Vert.x instance.
171+
try (VertxA2AHttpClient httpClient = new VertxA2AHttpClient()) {
172+
try (Client client = Client
173+
.builder(agentCard)
174+
.withTransport(JSONRPCTransport.class,
175+
new JSONRPCTransportConfig(httpClient))
176+
.build()) {
177+
// use client...
178+
}
179+
}
180+
```
181+
154182
### REST with a Custom HTTP Client
155183

156184
```java

‎extras/http-client-vertx/src/test/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClientIntegrationTest.java‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import org.junit.jupiter.api.BeforeEach;
1414
import org.junit.jupiter.api.Test;
1515
import org.mockserver.integration.ClientAndServer;
16+
import org.mockserver.verify.VerificationTimes;
1617

1718
public class VertxA2AHttpClientIntegrationTest {
1819

@@ -87,6 +88,28 @@ public void testDeleteRequestSuccess() throws Exception {
8788
assertTrue(response.success());
8889
}
8990

91+
@Test
92+
public void testCustomHostHeaderOverride() throws Exception {
93+
// Unlike the JDK-based client, VertxA2AHttpClient does not build on
94+
// java.net.http.HttpClient and so is not subject to its restricted-header
95+
// check: callers can override "Host" while still connecting to the URL's
96+
// own host:port (e.g. to reach an instance directly while presenting the
97+
// hostname a load balancer would normally supply).
98+
mockServer
99+
.when(request().withMethod("GET").withPath("/test"))
100+
.respond(response().withStatusCode(200).withBody("ok"));
101+
102+
A2AHttpResponse response = client.createGet()
103+
.url(getBaseUrl() + "/test")
104+
.addHeader("Host", "custom-logical-host.example.com")
105+
.get();
106+
107+
assertEquals(200, response.status());
108+
mockServer.verify(
109+
request().withPath("/test").withHeader("Host", "custom-logical-host.example.com"),
110+
VerificationTimes.once());
111+
}
112+
90113
@Test
91114
public void test401AuthenticationErrorOnGet() throws Exception {
92115
mockServer

‎http-client/src/main/java/org/a2aproject/sdk/client/http/JdkA2AHttpClient.java‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@
4646
* {@link org.a2aproject.sdk.client.http.A2AHttpClient.PostBuilder#followRedirects(boolean)
4747
* followRedirects(true)} on the builder. Both steps are required.
4848
*
49+
* <p><b>{@code Host} header limitation:</b> the underlying JDK {@link HttpClient}
50+
* rejects {@code addHeader("Host", ...)} with an {@code IllegalArgumentException}
51+
* ("restricted header name") unless the JVM is started with
52+
* {@code -Djdk.httpclient.allowRestrictedHeaders=host}. This is a JDK-wide
53+
* restriction that cannot be worked around per-client-instance. Applications that
54+
* need to send an explicit {@code Host} header (for example, connecting directly to
55+
* an instance while presenting the hostname a load balancer normally supplies) should
56+
* either set that system property or use an {@link A2AHttpClient} implementation that
57+
* does not build on {@code java.net.http.HttpClient}, such as
58+
* {@code org.a2aproject.sdk.client.http.vertx.VertxA2AHttpClient} (the
59+
* {@code a2a-java-sdk-http-client-vertx} extra), which has no such restriction.
60+
*
4961
* <p><b>Provider Priority:</b> 0 (lowest - used as fallback)
5062
*
5163
* <p>This implementation is registered via {@link JdkA2AHttpClientProvider}

‎http-client/src/test/java/org/a2aproject/sdk/client/http/JdkA2AHttpClientTest.java‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.net.http.HttpResponse;
1919
import java.net.http.WebSocket;
2020
import java.time.Duration;
21+
import java.util.Arrays;
2122
import java.util.List;
2223
import java.util.Map;
2324
import java.util.Optional;
@@ -39,6 +40,7 @@
3940
import static org.junit.jupiter.api.Assertions.assertNull;
4041
import static org.junit.jupiter.api.Assertions.assertThrows;
4142
import static org.junit.jupiter.api.Assertions.assertTrue;
43+
import static org.junit.jupiter.api.Assumptions.assumeFalse;
4244
import static org.mockserver.model.HttpRequest.request;
4345
import static org.mockserver.model.HttpResponse.response;
4446

@@ -69,6 +71,34 @@ public void testDefaultConstructorCreatesUsableClient() throws Exception {
6971
assertEquals("ok", response.body());
7072
}
7173

74+
@Test
75+
public void testCustomHostHeaderRejectedByJdkHttpClient() {
76+
// Documents a JDK-wide restriction (not specific to this SDK): java.net.http.HttpClient
77+
// rejects an explicit "Host" header unless the JVM is started with
78+
// -Djdk.httpclient.allowRestrictedHeaders=host. Callers who need to send a custom Host
79+
// header should use that JVM property or a non-JDK A2AHttpClient implementation, e.g.
80+
// VertxA2AHttpClient from the a2a-java-sdk-http-client-vertx extra.
81+
//
82+
// Skip if the JVM running this test already allows a restricted "Host" header: in that
83+
// (documented, supported) configuration the JDK client legitimately accepts it instead.
84+
assumeFalse(jdkAllowsRestrictedHostHeader(), "JVM allows a restricted Host header "
85+
+ "via jdk.httpclient.allowRestrictedHeaders; the JDK client no longer rejects it");
86+
87+
JdkA2AHttpClient client = new JdkA2AHttpClient();
88+
89+
assertThrows(IllegalArgumentException.class, () -> client.createGet()
90+
.url("http://localhost:1/unused")
91+
.addHeader("Host", "custom.example.com")
92+
.get());
93+
}
94+
95+
private static boolean jdkAllowsRestrictedHostHeader() {
96+
String allowed = System.getProperty("jdk.httpclient.allowRestrictedHeaders", "");
97+
return Arrays.stream(allowed.split(","))
98+
.map(String::trim)
99+
.anyMatch("host"::equalsIgnoreCase);
100+
}
101+
72102
@Test
73103
public void testConstructorUsesProvidedHttpClient() throws Exception {
74104
server = ClientAndServer.startClientAndServer(0);

0 commit comments

Comments
 (0)