vert.x
vert.x copied to clipboard
Vert.x does not permit to proxy HTTP/2 requests without `:authority` pseudo header correctly
Version
I used vert.x core 4.5.14
Context
The HTTP/2 RFC clearly says that an HTTP/2 request may have no :authority pseudo header:
Clients that generate HTTP/2 requests directly MUST use the ":authority" pseudo-header field to convey authority information, unless there is no authority information to convey (in which case it MUST NOT generate ":authority").
An intermediary that forwards a request over HTTP/2 MUST construct an ":authority" pseudo-header field using the authority information from the control data of the original request, unless the original request's target URI does not contain authority information (in which case it MUST NOT generate ":authority"). Note that the Host header field is not the sole source of this information; see Section 7.2 of [HTTP].
But:
- On server side, Vert.x automatically uses the
Hostheader as "authority" if no authority is present. Hiding the information that no authority was present in the request - On client side, Vert.x does not permit to generate HTTP/2 requests without authority.
These 2 points make the reverse-proxying of HTTP/2 requests without authority impossible.
Do you have a reproducer?
Not a real reproducer, but something to investigate HTTP/2 request management using Vert.x:
import java.util.List;
import java.util.concurrent.CountDownLatch;
import org.junit.jupiter.api.Test;
import io.vertx.core.Vertx;
import io.vertx.core.http.HttpClientOptions;
import io.vertx.core.http.HttpClientRequest;
import io.vertx.core.http.HttpMethod;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.http.HttpVersion;
import io.vertx.core.http.RequestOptions;
public class AuthorityTest {
@Test
void test() throws InterruptedException {
CountDownLatch serverLatch = new CountDownLatch(1);
Vertx vertx = Vertx.vertx();
HttpServerOptions serverOptions = new HttpServerOptions().setAlpnVersions(List.of(HttpVersion.HTTP_2))
.setUseAlpn(true)
.setHttp2ClearTextEnabled(true);
HttpServer server = vertx.createHttpServer(serverOptions)
.requestHandler(req -> {
System.out.println("received authority: " + req.authority());
req.response().setStatusCode(200).end();
});
server.listen(8082)
.onSuccess(unused -> serverLatch.countDown());
serverLatch.await(); // server started
CountDownLatch clientLatch = new CountDownLatch(1);
HttpClientOptions clientOptions = new HttpClientOptions().setProtocolVersion(HttpVersion.HTTP_2);
RequestOptions options = new RequestOptions()
.setMethod(HttpMethod.GET)
.setPort(8082)
.setHost("localhost")
.setURI("/");
vertx.createHttpClient(clientOptions)
.request(options)
.compose(HttpClientRequest::send)
.onSuccess(resp -> {
System.out.println("received response: " + resp.statusCode());
clientLatch.countDown();
})
.onFailure(Throwable::printStackTrace);
clientLatch.await();
server.close();
}
Extra
This is link to a discussion on an HA proxy issue regarding HTTP/1.1 -> HTTP/2 request translation: https://github.com/haproxy/haproxy/issues/2592
Which discussion led to a question raised for the W3C HTTP working group: https://lists.w3.org/Archives/Public/ietf-http-wg/2024JulSep/0258.html
maybe there should be an option where vertx allows pseudo headers to be used in a multimap headers for HTTP/2 that would be used in such case
That would allow inspecting this map on a response and have a precise knowledge of the actual headers sent by the remote endpoint.
I'm back on this issue, here is my thinking at API level (from what I saw it seems possible to update the code to do this)
On server side, we could add a new method like HttpRequestHead#pseudoHeaders() ? So we don't break anything but still have the information somewhere. And they are not mixed with "normal" http headers.
This could either throw or return a empty map in HTTP/1.x
On client side, maybe adding RequestOptions#setAuthorityPseudoHeader(String) so that we can force the :authority, and setting it to null would make sure no :authority header is present in the request. This wouldn't force the Host header because it can already be handled using header-related methods and may result in missing Host headers for HTTP/1.1 requests.
@vietj What do you think about these changes ?
I think instead we could have a boolean validate when getting an header map that returns a map that does not validate when adding headers
🤔 but a missing :authority is still valid for an HTTP/2 request right ? I don't want to get rid of validation here, but produce a valid message which won't be that of an edge case because it always happen on HTTP/1.x -> HTTP/2 -> HTTP/1.x scenario.
from the client perspective in 5.x (I don't know yet if that is valid for 4.x) we can allow to set a null authority on the client request that will avoid setting automatically the host header for HTTP/1.x and the authority pseudo header for HTTP/2
I've created a PR on 5.x with your idea on client side. Could you have a look ? @vietj I still don't understand your idea on server side:
I think instead we could have a boolean validate when getting an header map that returns a map that does not validate when adding headers
Could you explain ?
I've added a way to access to the authority pseudo header, let me know what you think about it.
@vietj I've updated my PR to only cover the server side now