vert.x icon indicating copy to clipboard operation
vert.x copied to clipboard

Vert.x does not permit to proxy HTTP/2 requests without `:authority` pseudo header correctly

Open NilsRenaud opened this issue 7 months ago • 1 comments

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").

Then it also say this:

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 Host header 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

NilsRenaud avatar Apr 16 '25 11:04 NilsRenaud

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.

vietj avatar Apr 17 '25 08:04 vietj

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 ?

NilsRenaud avatar Jul 08 '25 16:07 NilsRenaud

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

vietj avatar Jul 08 '25 17:07 vietj

🤔 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.

NilsRenaud avatar Jul 08 '25 19:07 NilsRenaud

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

vietj avatar Jul 09 '25 17:07 vietj

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 ?

NilsRenaud avatar Jul 23 '25 10:07 NilsRenaud

I've added a way to access to the authority pseudo header, let me know what you think about it.

NilsRenaud avatar Jul 24 '25 12:07 NilsRenaud

@vietj I've updated my PR to only cover the server side now

NilsRenaud avatar Jul 30 '25 13:07 NilsRenaud