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
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public class JSONRPCTransport implements ClientTransport {
private final AgentInterface agentInterface;
private final @Nullable List<ClientCallInterceptor> interceptors;
private final @Nullable AgentCard agentCard;
private final boolean followRedirects;

public JSONRPCTransport(String agentUrl) {
this(null, null, new AgentInterface("JSONRPC", agentUrl), null);
Expand All @@ -86,10 +87,17 @@ public JSONRPCTransport(AgentCard agentCard) {

public JSONRPCTransport(@Nullable A2AHttpClient httpClient, @Nullable AgentCard agentCard,
AgentInterface agentInterface, @Nullable List<ClientCallInterceptor> interceptors) {
this(httpClient, agentCard, agentInterface, interceptors, false);
}

public JSONRPCTransport(@Nullable A2AHttpClient httpClient, @Nullable AgentCard agentCard,
AgentInterface agentInterface, @Nullable List<ClientCallInterceptor> interceptors,
boolean followRedirects) {
this.httpClient = httpClient == null ? A2AHttpClientFactory.create(A2AHttpClientConfiguration.DEFAULT) : httpClient;
this.agentCard = agentCard;
this.agentInterface = agentInterface;
this.interceptors = interceptors;
this.followRedirects = followRedirects;
}

@Override
Expand Down Expand Up @@ -121,7 +129,8 @@ public void sendMessageStreaming(MessageSendParams request, Consumer<StreamingEv
SSEEventListener sseEventListener = new SSEEventListener(eventConsumer, errorConsumer);

try {
A2AHttpClient.PostBuilder builder = createPostBuilder(agentInterface.url(), payloadAndHeaders, SEND_STREAMING_MESSAGE_METHOD);
A2AHttpClient.PostBuilder builder = createPostBuilder(
agentInterface.url(), payloadAndHeaders, SEND_STREAMING_MESSAGE_METHOD, false);
ref.set(builder.postAsyncSSE(
event -> sseEventListener.onMessage(event, ref.get()),
throwable -> sseEventListener.onError(throwable, ref.get()),
Expand Down Expand Up @@ -271,7 +280,8 @@ public void subscribeToTask(TaskIdParams request, Consumer<StreamingEventKind> e
SSEEventListener sseEventListener = new SSEEventListener(eventConsumer, errorConsumer);

try {
A2AHttpClient.PostBuilder builder = createPostBuilder(agentInterface.url(), payloadAndHeaders, SUBSCRIBE_TO_TASK_METHOD);
A2AHttpClient.PostBuilder builder = createPostBuilder(
agentInterface.url(), payloadAndHeaders, SUBSCRIBE_TO_TASK_METHOD, false);
ref.set(builder.postAsyncSSE(
event -> sseEventListener.onMessage(event, ref.get()),
throwable -> sseEventListener.onError(throwable, ref.get()),
Expand Down Expand Up @@ -324,7 +334,7 @@ private PayloadAndHeaders applyInterceptors(String methodName, @Nullable Object
}

private String sendPostRequest(String url, PayloadAndHeaders payloadAndHeaders, String method) throws IOException, InterruptedException, JsonProcessingException {
A2AHttpClient.PostBuilder builder = createPostBuilder(url, payloadAndHeaders, method);
A2AHttpClient.PostBuilder builder = createPostBuilder(url, payloadAndHeaders, method, followRedirects);
A2AHttpResponse response = builder.post();
if (!response.success()) {
int status = response.status();
Expand All @@ -334,11 +344,14 @@ private String sendPostRequest(String url, PayloadAndHeaders payloadAndHeaders,
return response.body();
}

private A2AHttpClient.PostBuilder createPostBuilder(String url, PayloadAndHeaders payloadAndHeaders, String method) throws JsonProcessingException {
private A2AHttpClient.PostBuilder createPostBuilder(
String url, PayloadAndHeaders payloadAndHeaders, String method, boolean followRedirects)
throws JsonProcessingException {
A2AHttpClient.PostBuilder postBuilder = httpClient.createPost()
.url(url)
.addHeader("Content-Type", "application/json")
.addHeader(A2AHeaders.A2A_VERSION, AgentInterface.CURRENT_PROTOCOL_VERSION)
.followRedirects(followRedirects)
.body(JSONRPCUtils.toJsonRPCRequest(null, method, (MessageOrBuilder) payloadAndHeaders.getPayload()));

if (payloadAndHeaders.getHeaders() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
public class JSONRPCTransportConfig extends ClientTransportConfig<JSONRPCTransport> {

private final @Nullable A2AHttpClient httpClient;
private final boolean followRedirects;

/**
* Create a JSON-RPC transport configuration with the default HTTP client.
Expand All @@ -59,7 +60,7 @@ public class JSONRPCTransportConfig extends ClientTransportConfig<JSONRPCTranspo
* {@link JSONRPCTransportConfigBuilder} instead for a more fluent API.
*/
public JSONRPCTransportConfig() {
this.httpClient = null;
this(null, false);
}

/**
Expand All @@ -70,7 +71,18 @@ public JSONRPCTransportConfig() {
* @param httpClient the HTTP client to use for JSON-RPC requests
*/
public JSONRPCTransportConfig(A2AHttpClient httpClient) {
this(httpClient, false);
}

/**
* Create a JSON-RPC transport configuration with a custom HTTP client and redirect policy.
*
* @param httpClient the HTTP client to use for JSON-RPC requests
* @param followRedirects whether POST requests may follow a single redirect
*/
public JSONRPCTransportConfig(@Nullable A2AHttpClient httpClient, boolean followRedirects) {
this.httpClient = httpClient;
this.followRedirects = followRedirects;
}

/**
Expand All @@ -81,4 +93,13 @@ public JSONRPCTransportConfig(A2AHttpClient httpClient) {
public @Nullable A2AHttpClient getHttpClient() {
return httpClient;
}

/**
* Whether POST requests issued by this transport may follow a redirect.
*
* @return {@code true} when redirect following is enabled
*/
public boolean isFollowRedirects() {
return followRedirects;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public class JSONRPCTransportConfigBuilder extends ClientTransportConfigBuilder<

private @Nullable A2AHttpClient httpClient;
private A2AHttpClientConfiguration httpClientConfiguration = A2AHttpClientConfiguration.DEFAULT;
private boolean followRedirects;

/**
* Set the HTTP client to use for JSON-RPC requests.
Expand Down Expand Up @@ -107,6 +108,23 @@ public JSONRPCTransportConfigBuilder httpClientConfiguration(A2AHttpClientConfig
return this;
}

/**
* Enables explicit, one-hop redirect following for non-streaming POST requests.
*
* <p>Disabled by default. The configured {@link A2AHttpClient} remains responsible for
* applying its redirect-header policy. When using {@code JdkA2AHttpClient} with a supplied
* JDK client, that JDK client must have native redirects disabled.
*
* <p>Streaming SSE requests are never redirected.
*
* @param followRedirects whether non-streaming POST requests may follow a redirect
* @return this builder for method chaining
*/
public JSONRPCTransportConfigBuilder followRedirects(boolean followRedirects) {
this.followRedirects = followRedirects;
return this;
}

/**
* Build the JSON-RPC transport configuration.
* <p>
Expand All @@ -122,7 +140,7 @@ public JSONRPCTransportConfig build() {
httpClient = A2AHttpClientFactory.create(httpClientConfiguration);
}

JSONRPCTransportConfig config = new JSONRPCTransportConfig(httpClient);
JSONRPCTransportConfig config = new JSONRPCTransportConfig(httpClient, followRedirects);
config.setInterceptors(this.interceptors);
return config;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ public JSONRPCTransport create(@Nullable JSONRPCTransportConfig clientTransportC
currentClientTransportConfig = new JSONRPCTransportConfig(
A2AHttpClientFactory.create(A2AHttpClientConfiguration.DEFAULT));
}
return new JSONRPCTransport(currentClientTransportConfig.getHttpClient(), agentCard, agentInterface, currentClientTransportConfig.getInterceptors());
return new JSONRPCTransport(currentClientTransportConfig.getHttpClient(), agentCard, agentInterface,
currentClientTransportConfig.getInterceptors(), currentClientTransportConfig.isFollowRedirects());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,16 @@ private JsonObject listTasksAndGetRequestParams(ListTasksParams params) throws E
return json.getAsJsonObject("params");
}

@Test
public void configBuilderDisablesRedirectFollowingByDefault() {
assertFalse(new JSONRPCTransportConfigBuilder().build().isFollowRedirects());
}

@Test
public void configBuilderEnablesRedirectFollowing() {
assertTrue(new JSONRPCTransportConfigBuilder().followRedirects(true).build().isFollowRedirects());
}

@Test
public void testA2AClientSendMessage() throws Exception {
this.server.when(
Expand Down Expand Up @@ -843,4 +853,4 @@ public void testVersionNotSupportedErrorUnmarshalling() throws Exception {
assertTrue(versionError.getMessage().contains("1.0"));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,25 @@ public class RestTransport implements ClientTransport {
private final AgentInterface agentInterface;
private @Nullable final List<ClientCallInterceptor> interceptors;
private final AgentCard agentCard;
private final boolean followRedirects;

public RestTransport(AgentCard agentCard) {
this(null, agentCard, Utils.getFavoriteInterface(agentCard), null);
}

public RestTransport(@Nullable A2AHttpClient httpClient, AgentCard agentCard,
AgentInterface agentInterface, @Nullable List<ClientCallInterceptor> interceptors) {
this(httpClient, agentCard, agentInterface, interceptors, false);
}

public RestTransport(@Nullable A2AHttpClient httpClient, AgentCard agentCard,
AgentInterface agentInterface, @Nullable List<ClientCallInterceptor> interceptors,
boolean followRedirects) {
this.httpClient = httpClient == null ? A2AHttpClientFactory.create(A2AHttpClientConfiguration.DEFAULT) : httpClient;
this.agentCard = agentCard;
this.agentInterface = agentInterface;
this.interceptors = interceptors;
this.followRedirects = followRedirects;
}

@Override
Expand Down Expand Up @@ -117,7 +125,10 @@ public void sendMessageStreaming(MessageSendParams messageSendParams, Consumer<S
AtomicReference<CompletableFuture<Void>> ref = new AtomicReference<>();
SSEEventListener sseEventListener = new SSEEventListener(eventConsumer, errorConsumer);
try {
A2AHttpClient.PostBuilder postBuilder = createPostBuilder(Utils.buildBaseUrl(agentInterface, messageSendParams.tenant()) + "/message:stream", payloadAndHeaders);
A2AHttpClient.PostBuilder postBuilder = createPostBuilder(
Utils.buildBaseUrl(agentInterface, messageSendParams.tenant()) + "/message:stream",
payloadAndHeaders,
false);
ref.set(postBuilder.postAsyncSSE(
event -> sseEventListener.onMessage(event, ref.get()),
throwable -> sseEventListener.onError(throwable, ref.get()),
Expand Down Expand Up @@ -410,7 +421,7 @@ public void subscribeToTask(TaskIdParams request, Consumer<StreamingEventKind> e
SSEEventListener sseEventListener = new SSEEventListener(eventConsumer, errorConsumer);
try {
String url = Utils.buildBaseUrl(agentInterface, request.tenant()) + String.format("/tasks/%1s:subscribe", request.id());
A2AHttpClient.PostBuilder postBuilder = createPostBuilder(url, payloadAndHeaders);
A2AHttpClient.PostBuilder postBuilder = createPostBuilder(url, payloadAndHeaders, false);
ref.set(postBuilder.postAsyncSSE(
event -> sseEventListener.onMessage(event, ref.get()),
throwable -> sseEventListener.onError(throwable, ref.get()),
Expand Down Expand Up @@ -470,7 +481,7 @@ private String sendPostRequest(String url, PayloadAndHeaders payloadAndHeaders)
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine(body);
}
A2AHttpClient.PostBuilder builder = buildPost(url, payloadAndHeaders, body);
A2AHttpClient.PostBuilder builder = buildPost(url, payloadAndHeaders, body, followRedirects);
A2AHttpResponse response = builder.post();
if (!response.success()) {
if (LOGGER.isLoggable(Level.FINE)) {
Expand All @@ -481,19 +492,24 @@ private String sendPostRequest(String url, PayloadAndHeaders payloadAndHeaders)
return response.body();
}

private A2AHttpClient.PostBuilder createPostBuilder(String url, PayloadAndHeaders payloadAndHeaders) throws JsonProcessingException, InvalidProtocolBufferException {
private A2AHttpClient.PostBuilder createPostBuilder(
String url, PayloadAndHeaders payloadAndHeaders, boolean followRedirects)
throws JsonProcessingException, InvalidProtocolBufferException {
String body = ProtoJsonUtils.toJson(JsonFormat.printer(), (MessageOrBuilder) payloadAndHeaders.getPayload());
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine(body);
}
return buildPost(url, payloadAndHeaders, body);
return buildPost(url, payloadAndHeaders, body, followRedirects);
}

private A2AHttpClient.PostBuilder buildPost(String url, PayloadAndHeaders payloadAndHeaders, String body) throws JsonProcessingException {
private A2AHttpClient.PostBuilder buildPost(
String url, PayloadAndHeaders payloadAndHeaders, String body, boolean followRedirects)
throws JsonProcessingException {
A2AHttpClient.PostBuilder postBuilder = httpClient.createPost()
.url(url)
.addHeader("Content-Type", "application/json")
.addHeader(A2AHeaders.A2A_VERSION, AgentInterface.CURRENT_PROTOCOL_VERSION)
.followRedirects(followRedirects)
.body(body);

if (payloadAndHeaders.getHeaders() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
public class RestTransportConfig extends ClientTransportConfig<RestTransport> {

private final @Nullable A2AHttpClient httpClient;
private final boolean followRedirects;

/**
* Create a REST transport configuration with the default HTTP client.
Expand All @@ -59,7 +60,7 @@ public class RestTransportConfig extends ClientTransportConfig<RestTransport> {
* {@link RestTransportConfigBuilder} instead for a more fluent API.
*/
public RestTransportConfig() {
this.httpClient = null;
this(null, false);
}

/**
Expand All @@ -70,7 +71,18 @@ public RestTransportConfig() {
* @param httpClient the HTTP client to use for REST requests
*/
public RestTransportConfig(A2AHttpClient httpClient) {
this(httpClient, false);
}

/**
* Create a REST transport configuration with a custom HTTP client and redirect policy.
*
* @param httpClient the HTTP client to use for REST requests
* @param followRedirects whether POST requests may follow a single redirect
*/
public RestTransportConfig(@Nullable A2AHttpClient httpClient, boolean followRedirects) {
this.httpClient = httpClient;
this.followRedirects = followRedirects;
}

/**
Expand All @@ -81,4 +93,13 @@ public RestTransportConfig(A2AHttpClient httpClient) {
public @Nullable A2AHttpClient getHttpClient() {
return httpClient;
}
}

/**
* Whether POST requests issued by this transport may follow a redirect.
*
* @return {@code true} when redirect following is enabled
*/
public boolean isFollowRedirects() {
return followRedirects;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public class RestTransportConfigBuilder extends ClientTransportConfigBuilder<Res

private @Nullable A2AHttpClient httpClient;
private A2AHttpClientConfiguration httpClientConfiguration = A2AHttpClientConfiguration.DEFAULT;
private boolean followRedirects;

/**
* Set the HTTP client to use for REST requests.
Expand Down Expand Up @@ -107,6 +108,23 @@ public RestTransportConfigBuilder httpClientConfiguration(A2AHttpClientConfigura
return this;
}

/**
* Enables explicit, one-hop redirect following for non-streaming POST requests.
*
* <p>Disabled by default. The configured {@link A2AHttpClient} remains responsible for
* applying its redirect-header policy. When using {@code JdkA2AHttpClient} with a supplied
* JDK client, that JDK client must have native redirects disabled.
*
* <p>Streaming SSE requests are never redirected.
*
* @param followRedirects whether non-streaming POST requests may follow a redirect
* @return this builder for method chaining
*/
public RestTransportConfigBuilder followRedirects(boolean followRedirects) {
this.followRedirects = followRedirects;
return this;
}

/**
* Build the REST transport configuration.
* <p>
Expand All @@ -122,7 +140,7 @@ public RestTransportConfig build() {
httpClient = A2AHttpClientFactory.create(httpClientConfiguration);
}

RestTransportConfig config = new RestTransportConfig(httpClient);
RestTransportConfig config = new RestTransportConfig(httpClient, followRedirects);
config.setInterceptors(this.interceptors);
return config;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public RestTransport create(RestTransportConfig clientTransportConfig, AgentCard
if (transportConfig == null) {
transportConfig = new RestTransportConfig(A2AHttpClientFactory.create(A2AHttpClientConfiguration.DEFAULT));
}
return new RestTransport(transportConfig.getHttpClient(), agentCard, agentInterface, transportConfig.getInterceptors());
return new RestTransport(transportConfig.getHttpClient(), agentCard, agentInterface,
transportConfig.getInterceptors(), transportConfig.isFollowRedirects());
}

@Override
Expand Down
Loading
Loading