Skip to content

Commit a5e76a0

Browse files
fix(http-client-vertx): resolve relative redirect Location against the request URI
* fix(http-client-vertx): resolve relative redirect Location against the request URI The opt-in redirect follower in VertxA2AHttpClient passed the raw Location header value straight to WebClient.getAbs()/postAbs(). RFC 9110 (10.2.2) and RFC 7231 permit a relative Location such as /collect, so a relative value was handed to an absolute-URI request and failed with MalformedURLException instead of being followed. Resolve the Location against the original request URI using RFC 3986 (5) reference resolution (URI.create(url).resolve(location)) before building the redirected request. Absolute Location values are returned unchanged, so existing behavior is preserved; a malformed base or location falls back to the raw value. No redirect-target validation is added or weakened. Adds a Vert.x redirect test that serves a relative Location from a single origin and asserts it is resolved and followed; the inherited absolute-Location tests continue to pass. Fixes a2aproject#1142. Signed-off-by: AlgoVoi <chopmob@gmail.com>
1 parent ec5994a commit a5e76a0

3 files changed

Lines changed: 149 additions & 2 deletions

File tree

‎extras/http-client-vertx/src/main/java/org/a2aproject/sdk/client/http/vertx/VertxA2AHttpClient.java‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import jakarta.enterprise.inject.spi.BeanManager;
2626
import jakarta.enterprise.inject.spi.CDI;
2727
import java.io.IOException;
28+
import java.net.URI;
2829
import java.nio.charset.StandardCharsets;
2930
import java.util.HashMap;
3031
import java.util.List;
@@ -533,6 +534,28 @@ public CompletableFuture<Void> getAsyncSSE(
533534
}
534535
}
535536

537+
/**
538+
* Resolves an HTTP {@code Location} header value against the original request URI.
539+
*
540+
* <p>Per RFC 9110 10.2.2 (and RFC 7231 before it) a {@code Location} may be a relative
541+
* reference such as {@code /collect}; RFC 3986 5 reference resolution turns it into the
542+
* effective target URI. An already-absolute {@code Location} is returned unchanged. A
543+
* malformed base or location falls back to the raw value so any existing handling still
544+
* applies.
545+
*
546+
* @param requestUri the original (absolute) request URI the redirect responded to
547+
* @param location the raw {@code Location} header value
548+
* @return the absolute redirect target
549+
*/
550+
// Package-private for direct unit testing of RFC 3986 reference resolution.
551+
static String resolveRedirectLocation(String requestUri, String location) {
552+
try {
553+
return URI.create(requestUri).resolve(location).toString();
554+
} catch (IllegalArgumentException e) {
555+
return location;
556+
}
557+
}
558+
536559
private class VertxPostBuilder extends VertxBuilder<PostBuilder> implements A2AHttpClient.PostBuilder {
537560

538561
private String body = "";
@@ -571,12 +594,15 @@ public A2AHttpResponse post() throws IOException, InterruptedException {
571594
int statusCode = response.status();
572595
String location = response.headers().firstValue("Location");
573596
if (location != null) {
597+
// RFC 9110 10.2.2 permits a relative Location (e.g. /collect); resolve it
598+
// against the original request URI per RFC 3986 5 before following.
599+
String resolvedLocation = resolveRedirectLocation(url, location);
574600
if (statusCode == 301 || statusCode == 302 || statusCode == 303) {
575601
// RFC 7231: 301/302/303 redirect POST as GET; 307/308 would preserve the method
576-
return executeSyncRequest(webClient.getAbs(location), headers, null);
602+
return executeSyncRequest(webClient.getAbs(resolvedLocation), headers, null);
577603
} else if (statusCode == 307 || statusCode == 308) {
578604
// RFC 7538: 307/308 must repeat the request with the original method and body
579-
return executeSyncRequest(webClient.postAbs(location), headers, bodyBuffer);
605+
return executeSyncRequest(webClient.postAbs(resolvedLocation), headers, bodyBuffer);
580606
}
581607
}
582608
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,58 @@
11
package org.a2aproject.sdk.client.http.vertx;
22

3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.mockserver.model.HttpRequest.request;
5+
import static org.mockserver.model.HttpResponse.response;
6+
import static org.mockserver.verify.VerificationTimes.exactly;
7+
38
import org.a2aproject.sdk.client.http.A2AHttpClient;
9+
import org.a2aproject.sdk.client.http.A2AHttpResponse;
410
import org.a2aproject.sdk.client.http.AbstractA2AHttpClientRedirectTest;
11+
import org.junit.jupiter.api.AfterEach;
12+
import org.junit.jupiter.api.Test;
13+
import org.mockserver.integration.ClientAndServer;
514

615
public class VertxA2AHttpClientRedirectTest extends AbstractA2AHttpClientRedirectTest {
716

17+
private ClientAndServer relativeServer;
18+
819
@Override
920
protected A2AHttpClient createClient() {
1021
return new VertxA2AHttpClient();
1122
}
23+
24+
@AfterEach
25+
public void stopRelativeServer() {
26+
if (relativeServer != null) {
27+
relativeServer.stop();
28+
}
29+
}
30+
31+
/**
32+
* A relative {@code Location} value (permitted by RFC 9110 10.2.2, resolved per RFC 3986 5)
33+
* must be resolved against the original request URI and followed, rather than handed verbatim
34+
* to an absolute request where it fails. The redirect target is served by the same origin so
35+
* that {@code /collect} only resolves correctly when the base URI is applied.
36+
*
37+
* <p>Regression test for a2aproject/a2a-java#1142.
38+
*/
39+
@Test
40+
public void synchronousPostFollowsRelativeRedirectLocation() throws Exception {
41+
relativeServer = ClientAndServer.startClientAndServer(0);
42+
relativeServer.when(request().withMethod("POST").withPath("/agent"))
43+
.respond(response().withStatusCode(302).withHeader("Location", "/collect"));
44+
relativeServer.when(request().withPath("/collect"))
45+
.respond(response().withStatusCode(200).withBody("redirected"));
46+
47+
A2AHttpResponse result = createClient().createPost()
48+
.url("http://127.0.0.1:" + relativeServer.getLocalPort() + "/agent")
49+
.addHeader("X-API-Key", "FULCRUM-SYNTHETIC-RELATIVE")
50+
.body("{}")
51+
.followRedirects(true)
52+
.post();
53+
54+
assertEquals(200, result.status(),
55+
"Synchronous POST must resolve and follow a relative Location against the request URI");
56+
relativeServer.verify(request().withPath("/collect"), exactly(1));
57+
}
1258
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package org.a2aproject.sdk.client.http.vertx;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
/**
8+
* Unit tests for {@link VertxA2AHttpClient#resolveRedirectLocation(String, String)}, the RFC 3986
9+
* section 5 reference resolution applied to a redirect {@code Location} before the redirected
10+
* request is built.
11+
*
12+
* <p>Covers a2aproject/a2a-java#1142: a relative {@code Location} must resolve against the original
13+
* request URI rather than be handed verbatim to an absolute request.
14+
*/
15+
public class VertxA2AHttpClientResolveRedirectLocationTest {
16+
17+
@Test
18+
public void absolutePathResolvesAgainstOrigin() {
19+
assertEquals(
20+
"http://host:3000/collect",
21+
VertxA2AHttpClient.resolveRedirectLocation("http://host:3000/rpc/send", "/collect"));
22+
}
23+
24+
@Test
25+
public void relativePathResolvesAgainstCurrentDirectory() {
26+
assertEquals(
27+
"http://host:3000/rpc/collect",
28+
VertxA2AHttpClient.resolveRedirectLocation("http://host:3000/rpc/send", "collect"));
29+
}
30+
31+
@Test
32+
public void relativePathWithQueryIsResolved() {
33+
assertEquals(
34+
"http://host:3000/a/c?x=1",
35+
VertxA2AHttpClient.resolveRedirectLocation("http://host:3000/a/b", "c?x=1"));
36+
}
37+
38+
@Test
39+
public void dotSegmentsAreResolved() {
40+
assertEquals(
41+
"http://host:3000/a/d",
42+
VertxA2AHttpClient.resolveRedirectLocation("http://host:3000/a/b/c", "../d"));
43+
}
44+
45+
@Test
46+
public void absoluteLocationIsReturnedUnchanged() {
47+
assertEquals(
48+
"https://elsewhere.example/xyz",
49+
VertxA2AHttpClient.resolveRedirectLocation(
50+
"http://host:3000/rpc", "https://elsewhere.example/xyz"));
51+
}
52+
53+
@Test
54+
public void protocolRelativeLocationKeepsScheme() {
55+
assertEquals(
56+
"https://elsewhere.example/p",
57+
VertxA2AHttpClient.resolveRedirectLocation(
58+
"https://host/rpc", "//elsewhere.example/p"));
59+
}
60+
61+
@Test
62+
public void malformedLocationFallsBackToRawValue() {
63+
// A space is illegal in a URI reference, so URI.create throws and the raw
64+
// Location is returned so any existing downstream handling still applies.
65+
String raw = "/pa th";
66+
assertEquals(raw, VertxA2AHttpClient.resolveRedirectLocation("http://host/rpc", raw));
67+
}
68+
69+
@Test
70+
public void malformedBaseFallsBackToRawLocation() {
71+
String location = "/collect";
72+
assertEquals(
73+
location, VertxA2AHttpClient.resolveRedirectLocation("ht tp://host/rpc", location));
74+
}
75+
}

0 commit comments

Comments
 (0)