From 59304b0a3357226c9b1b7695ea372c1b873c36b7 Mon Sep 17 00:00:00 2001 From: Julien Viet Date: Wed, 1 Jul 2026 09:01:12 +0200 Subject: [PATCH] Correctly propagate the client request authority. Motivation: The HttpClientRequest authority should be properly set instead of being inherited from the connection authority. In addition we should let the client request to have no authority when desired (which can be desired to craft HTTP/2 without an authority). Changes: Correctly create the initial request authority from the options authority or the push authority pseudo header. Add test for these cases. --- .../io/vertx/core/http/HttpClientRequest.java | 18 +++++++-- .../vertx/core/http/impl/HttpClientImpl.java | 13 ++++--- .../vertx/core/http/impl/HttpClientPush.java | 5 ++- .../core/http/impl/HttpClientRequestBase.java | 13 +++++-- .../core/http/impl/HttpClientRequestImpl.java | 4 +- .../impl/HttpClientRequestPushPromise.java | 4 +- .../impl/UnpooledHttpClientConnection.java | 11 +++++- .../java/io/vertx/tests/http/HttpTest.java | 38 +++++++++++++++++++ 8 files changed, 87 insertions(+), 19 deletions(-) diff --git a/vertx-core/src/main/java/io/vertx/core/http/HttpClientRequest.java b/vertx-core/src/main/java/io/vertx/core/http/HttpClientRequest.java index 4387a9a6795..5b955e233d0 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/HttpClientRequest.java +++ b/vertx-core/src/main/java/io/vertx/core/http/HttpClientRequest.java @@ -65,11 +65,16 @@ public interface HttpClientRequest extends WriteStream { HttpClientRequest drainHandler(Handler handler); /** - * Override the request authority, when using HTTP/1.x this overrides the request {@code host} header, when using - * HTTP/2 this sets the {@code authority} pseudo header. When the port is a negative value, the default - * scheme port will be used. + * Override the request authority initially set by the client. * - *

The default request authority is the server host and port when connecting to the server. + *

    + *
  • when using HTTP/1.x this overrides the request {@code host} header
  • + *
  • when using HTTP/2 this sets the {@code authority} pseudo header
  • + * + * When the port is a negative value, the default scheme port will be used. + * + *

    According to the protocol, it can set to {@code null} (although it is not common), e.g. an HTTP/2 request + * can have a null authority when it carries an {@code host} header.

    * * @param authority override the request authority * @return a reference to this, so the API can be used fluently @@ -77,6 +82,11 @@ public interface HttpClientRequest extends WriteStream { @Fluent HttpClientRequest authority(HostAndPort authority); + /** + * @return the current request authority + */ + HostAndPort authority(); + /** * Set the request to follow HTTP redirects up to {@link HttpClientOptions#getMaxRedirects()}. * diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientImpl.java b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientImpl.java index 9ec95bef2a7..7aa63b9e92a 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientImpl.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientImpl.java @@ -527,7 +527,7 @@ private Future doRequestDirectly( }); }); }); - return wrap(httpMethod, requestURI, headers, traceOperation, idleTimeout, followRedirects, proxyOptions, fut2); + return wrap(authority, httpMethod, requestURI, headers, traceOperation, idleTimeout, followRedirects, proxyOptions, fut2); } private Future doRequest( @@ -698,7 +698,7 @@ public Future apply(SharedHttpClientConnectionGroup po // I think this is not possible - so remove it return streamCtx.failedFuture("Cannot resolve address " + server); } else { - return wrap(method, requestURI, headers, traceOperation, idleTimeout, followRedirects, null, future); + return wrap(authority, method, requestURI, headers, traceOperation, idleTimeout, followRedirects, null, future); } } @@ -738,7 +738,8 @@ Future getPool(boolean resolveOrigin, return resourceManager.withResourceAsync(key, provider, (group, created) -> function.apply(group)); } - private Future wrap(HttpMethod method, + private Future wrap(HostAndPort authority, + HttpMethod method, String requestURI, MultiMap headers, String traceOperation, @@ -756,7 +757,7 @@ private Future wrap(HttpMethod method, options.setFollowRedirects(followRedirects); options.setTraceOperation(traceOperation); HttpClientStream stream = res.stream; - HttpClientRequestImpl request = createRequest(stream.connection(), stream, options); + HttpClientRequestImpl request = createRequest(authority, stream.connection(), stream, options); if (res.alternative != null) { String altUsedValue; int defaultPort = stream.connection().isSsl() ? 443 : 80; @@ -787,8 +788,8 @@ public ConnectionObtainedResult(HttpClientStream stream, Lease> rHandler = redirectHandler; if (rHandler != null) { diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientPush.java b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientPush.java index c21e6be74ff..87f33f83541 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientPush.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientPush.java @@ -48,6 +48,10 @@ public HttpClientStream stream() { return stream; } + public HostAndPort authority() { + return authority; + } + @Override public long id() { return stream.id(); @@ -82,5 +86,4 @@ public String uri() { public HttpMethod method() { return method; } - } diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestBase.java b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestBase.java index ebd72d844ad..e68645accf7 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestBase.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestBase.java @@ -45,14 +45,14 @@ public abstract class HttpClientRequestBase implements HttpClientRequestInternal private long lastDataReceived; protected Throwable reset; - HttpClientRequestBase(HttpConnection connection, HttpClientStream stream, PromiseInternal responsePromise, HttpMethod method, String uri) { + HttpClientRequestBase(HostAndPort authority, HttpConnection connection, HttpClientStream stream, PromiseInternal responsePromise, HttpMethod method, String uri) { this.connection = connection; this.stream = stream; this.responsePromise = responsePromise; this.context = responsePromise.context(); this.uri = uri; this.method = method; - this.authority = stream.connection().authority(); + this.authority = authority; this.ssl = stream.connection().isSsl(); // @@ -70,7 +70,7 @@ public abstract class HttpClientRequestBase implements HttpClientRequestInternal }); } - protected HostAndPort authority() { + public HostAndPort authority() { return authority; } @@ -171,7 +171,12 @@ void fail(Throwable t) { } void handlePush(HttpClientPush push) { - HttpClientRequestPushPromise pushReq = new HttpClientRequestPushPromise(connection, push.stream(), push.method(), push.uri(), push.headers()); + HttpClientRequestPushPromise pushReq = new HttpClientRequestPushPromise(connection, + push.stream(), + push.authority(), + push.method(), + push.uri(), + push.headers()); if (pushHandler != null) { pushHandler.handle(pushReq); } else { diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestImpl.java b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestImpl.java index 42a9795caf9..8fddd226ffa 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestImpl.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestImpl.java @@ -59,8 +59,8 @@ public class HttpClientRequestImpl extends HttpClientRequestBase implements Http private boolean isConnect; private String traceOperation; - public HttpClientRequestImpl(HttpConnection connection, HttpClientStream stream) { - super(connection, stream, stream.context().promise(), HttpMethod.GET, "/"); + public HttpClientRequestImpl(HostAndPort authority, HttpConnection connection, HttpClientStream stream) { + super(authority, connection, stream, stream.context().promise(), HttpMethod.GET, "/"); this.chunked = false; this.endPromise = context.promise(); this.endFuture = endPromise.future(); diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestPushPromise.java b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestPushPromise.java index 16a27c41e4e..e19046751b7 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestPushPromise.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestPushPromise.java @@ -18,6 +18,7 @@ import io.vertx.core.Promise; import io.vertx.core.buffer.Buffer; import io.vertx.core.http.*; +import io.vertx.core.net.HostAndPort; import java.util.function.Function; @@ -32,10 +33,11 @@ class HttpClientRequestPushPromise extends HttpClientRequestBase { public HttpClientRequestPushPromise( HttpConnection connection, HttpClientStream stream, + HostAndPort authority, HttpMethod method, String uri, MultiMap headers) { - super(connection, stream, stream.connection().context().promise(), method, uri); + super(authority, connection, stream, stream.connection().context().promise(), method, uri); this.stream = stream; this.headers = headers; } diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/UnpooledHttpClientConnection.java b/vertx-core/src/main/java/io/vertx/core/http/impl/UnpooledHttpClientConnection.java index 3cd61aa54f2..ca81d1142e3 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/UnpooledHttpClientConnection.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/UnpooledHttpClientConnection.java @@ -20,6 +20,7 @@ import io.vertx.core.http.*; import io.vertx.core.internal.ContextInternal; import io.vertx.core.internal.PromiseInternal; +import io.vertx.core.net.HostAndPort; import io.vertx.core.net.SocketAddress; import javax.net.ssl.SSLSession; @@ -231,8 +232,16 @@ public Future request(ContextInternal context, RequestOptions future = actual.createStream(context); } } + String host = options.getHost(); + Integer port = options.getPort(); + HostAndPort authority; + if (host != null && port != null) { + authority = HostAndPort.authority(host, port); + } else { + authority = actual.authority(); + } return future.map(stream -> { - HttpClientRequestImpl request = new HttpClientRequestImpl(this, stream); + HttpClientRequestImpl request = new HttpClientRequestImpl(authority, this, stream); stream.closeHandler(this::checkPending); if (options != null) { request.init(options); diff --git a/vertx-core/src/test/java/io/vertx/tests/http/HttpTest.java b/vertx-core/src/test/java/io/vertx/tests/http/HttpTest.java index 9d64540d49e..a2aca76201a 100644 --- a/vertx-core/src/test/java/io/vertx/tests/http/HttpTest.java +++ b/vertx-core/src/test/java/io/vertx/tests/http/HttpTest.java @@ -44,6 +44,7 @@ import io.vertx.tests.http.http3.Http3Test; import org.assertj.core.api.AbstractThrowableAssert; import org.junit.Assert; +import org.junit.Assume; import org.junit.Test; import java.io.*; @@ -3771,6 +3772,7 @@ class MockReq implements HttpClientRequest { public HttpClientRequest setChunked(boolean chunked) { throw new UnsupportedOperationException(); } public boolean isChunked() { return false; } public HttpClientRequest authority(HostAndPort authority) { throw new UnsupportedOperationException(); } + public HostAndPort authority() { throw new UnsupportedOperationException(); } public HttpMethod getMethod() { return method; } public String absoluteURI() { return baseURI; } public HttpVersion version() { return HttpVersion.HTTP_1_1; } @@ -6124,4 +6126,40 @@ public void testResolverKeepAlive() throws Exception { assertTrue(delta >= 500); assertTrue(delta <= 1000); } + + @Test + public void testNoAuthority() throws Exception { + Assume.assumeTrue(testAddress.isInetSocket()); + assertEquals(testAddress.toString(), testNoAuthority(false).toString()); + } + + @Test + public void testForceNoAuthority() throws Exception { + Assume.assumeTrue(testAddress.isInetSocket() && config.version() != HttpVersion.HTTP_3); + assertEquals("null", testNoAuthority(true).toString()); + } + + public Buffer testNoAuthority(boolean force) throws Exception { + server.requestHandler(request -> { + request + .response() + .end("" + request.authority()); + }); + + startServer(testAddress); + + return client + .request(new RequestOptions().setServer(testAddress)) + .compose(request -> { + assertEquals(testAddress.toString(), request.authority().toString()); + if (force) { + request.authority(null); + } + return request + .send() + .expecting(HttpResponseExpectation.SC_OK) + .compose(HttpClientResponse::body); + }) + .await(); + } }