Last Updated on March 5, 2022


HTTP/2 has been widely adopted since the first publication of the standard in 2015. Based on the data published by the HTTP Archive, HTTP/2 requests represent more than 65% of HTTP requests in 2021.  

HTTP/2 usage rate from httparchive.org
HTTP/2 usage rate from httparchive.org

.NET developers have the possibility to enable HTTP/2 with HttpClient since the release of .NET Core 3.0. This client-side support of HTTP/2 came later after the server-side support, introduced in ASP .NET Core 2.2

Since .NET 5.0, Microsoft added more HTTP/2 options to the HttpClient object. These options allow developers to select the desired behavior for protocol versions downgrade and upgrade decisions. In fact, these options are available in .NET 6.0 as well, and we will look at them in detail in this article.

If you have never used the HttpClient class before, you can find more information about it here.

What is HTTP/2?

The term HTTP/2 refers to version 2 of the Hypertext Transfer Protocol (HTTP). Before HTTP/2, HTTP/1.1 has been the predominant version of the standard for almost 20 years.

HTTP/2 addresses different issues identified in HTTP 1/1, such as the famous head-of-line blocking limitation that can create congestion and reduce the performance of the communication channel. In order to mitigate this issue, HTTP/1.1 clients need to open multiple TCP connections when sending concurrent requests to the same server. It’s not an ideal solution since there is a limit to the number of TCP connection that you can open on a machine. However, that issue has been drastically reduced in HTTP/2. Furthermore, the upcoming version of the standard (HTTP/3 with QUIC protocol) will completely fix it.

In a nutshell, HTTP/2 originally started as the SPDY (“speedy”) protocol, developed by Google to reduce web page load latency. In order to put the HTTP/2 standard in place, the Internet Engineering Task Force (IETF) leveraged and improved the different techniques already used in SPDY like multiplexing, data prioritization, and compression.

Now let’s look at some of the important features available in HTTP/2.

Binary message framing

In HTTP/2, requests and responses are split into frames before being sent through the TCP connection. A frame is just a small binary chunk of data. In other words, a message is split into frames that are sent through a stream. A single TCP connection contains multiple streams of data. This multiplexing mechanism gives more flexibility during data transfer and improves the overall performance of the communication channel.

Stream prioritization

This feature allows clients to assign priorities to stream and therefore decide how the system will allocate resources when managing concurrent streams.

Header compression

HTTP headers go through the same binary message framing mechanism and therefore are packaged into frames as well. The HTTP/2 standard uses the HPACK specification as the compression format for HTTP Headers.

Server push

With HTTP/2, the server can be proactive and send resources to a client before receiving a request for that resource. For example, after sending an HTML page to a client, the server can decide to automatically push all referenced CSS files as well, instead of waiting for the browser to request these CSS files.

Now, let’s see how to configure HttpClient to use HTTP/2 standard.

Configuring HttpClient to use HTTP/2 in .NET 6.0

HTTP/2 connections are part of the application layer of the OSI model, therefore they sit on top of TCP connections. In addition, you can initiate an HTTP/2 with one of the 2 URL schemes used in HTTP/1.1: HTTP or HTTPS. These 2 schemes are at the origin of the 2 configuration identifiers of the HTTP/2 standard: h2 and h2c.

h2

The term “h2” refers to an HTTP/2 connection that is using the Transport Layer Security protocol (TLS).  In order to establish an HTTP/2 connection over TLS, applications need to rely on the TLS application-layer protocol negotiation (ALPN) extension. This mechanism simplifies the negotiation establishment because the HTTP/2 negotiation is directly integrated into TLS negotiation. That’s one of the reasons why most browsers support HTTP/2 with TLS only, because it’s easier to implement than HTTP/2 over cleartext.

h2c

The term “h2c” refers to an HTTP/2 connection over cleartext, meaning over a simple TCP connection. Basically, all data in this connection is exchanged in cleartext in a regular TCP connection. In this case, the HTTP version negotiation is achieved by leveraging the HTTP/1.1 Upgrade header. The connection starts as an HTTP/1.1 connection and then the client sends an Upgrade header to the server to upgrade the connection to HTTP/2. There is no guarantee that the server will accept to upgrade the connection. Therefore, establishing an h2c connection is more complicated than establishing an h2 connection.

Since .NET 5.0, Microsoft gave more flexibility to developers to configure the HttpClient to use h2 or h2c. Moreover, you have the option to decide how to handle the connection upgrades or downgrades procedures. Here are the 2 properties that are available for this purpose:

  • DefaultRequestVersion: Specifies the default HTTP version. The default value is HTTP 1.1, so you need to change it to HTTP 2 if you want to use the version 2 of the protocol.
  • DefaultVersionPolicy: Specifies the HttpVersionPolicy to use. There are 3 options for this property: RequestVersionOrLower, RequestVersionOrHigher and RequestVersionExact. The default value is RequestVersionOrLower, which means that the HttpClient will try to use the requested version and if that is not possible, it will downgrade to a lower version.

Now let’s try to use these options in a test application.

Configuring HttpClient to use HTTP/2 h2 connections

In this section, we will create a basic console application that uses the HttpClient to send an HTTP GET request to a server that supports HTTP/2 protocol with TLS. For this test, we will use the https://http2.akamai.com/demo website which has for purpose to demonstrate the impact of HTTP/2 on the download speed of a web page. In order to quickly validate if a server supports HTTP/2, you can use the KeyCDN website. For this specific example, here is the output of the KeyCDN tool for the http2.akamai.com website.

KeyCDN HTTP/2 Test
KeyCDN HTTP/2 Test

Now the following code will send a GET request and display the result in the console.

static async Task Main(string[] args)
{
    HttpClient myHttpClient = new HttpClient
    {
        DefaultRequestVersion = HttpVersion.Version20,
        DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrLower
    };

    string requestUrl = "https://http2.akamai.com/demo";

    try
    {
        Console.WriteLine($"GET {requestUrl}.");

        HttpResponseMessage response = await myHttpClient.GetAsync(requestUrl);
        response.EnsureSuccessStatusCode();

        Console.WriteLine($"Response HttpVersion: {response.Version}");

        string responseBody = await response.Content.ReadAsStringAsync();
        Console.WriteLine($"Response Body Length is: {responseBody.Length}");
        Console.WriteLine($"------------Response Body------------");
        Console.WriteLine(responseBody);
        Console.WriteLine($"------------End of Response Body------------");

    }
    catch (HttpRequestException e)
    {
        Console.WriteLine($"HttpRequestException : {e.Message}");
    }

    Console.WriteLine($"Press Enter to exit....");
    Console.ReadLine();
}

This gave the following output:

h2 connections test result

As you noticed in the logs, the protocol version is 2.0 as expected, since we configured the DefaultRequestVersion to 2.0 and the DefaultVersionPolicy to RequestVersionOrLower. In addition, we know that the server supports HTTP/2 over TLS, so there is no need for the HttpClient to downgrade the protocol version. The h2 connection is negotiated via ALPN.

Furthermore, you can update the code sample to use the other 2 possible values for the version policy: RequestVersionOrHigher and RequestVersionExact. In both cases, you will have the exact same result as RequestVersionOrLower if the server supports only HTTP/2 and HTTP/1.1 (and not HTTP/3).




Configuring HttpClient to use HTTP/2 h2c connections

Configuring HttpClient to use h2c is a little more tricky than configuring it to use h2. With the regular h2c configuration, there is no guarantee that the communication protocol will really end-up being HTTP/2 after the negotiation step. In such a case, the client needs to establish the connection in HTTP/1.1 first and then upgrades it to HTTP/2 by leveraging the Upgrade header. This process can generate more round-trips between the client and the server. Therefore, it can be quite complex if the first request is a POST request for example. Because of this complexity, the majority of web browsers added only the support for h2, and not h2c.  Nonetheless, it’s exactly the same reason why the HttpClient does not support upgrading an HTTP/1.1 connection to HTTP/2 over a clear text channel in .NET 5.0 and .NET 6.0.

However, if you know in advance that the server supports HTTP/2, then you can configure the HttpClient object to start the communication directly with the HTTP/2 protocol: that removes the need to use the Upgrade header in HTTP/1.1, and really simplifies the connection establishment procedure.

Since .NET 5.0, HttpClient allows you to connect with h2c to an HTTP/2 server if you know in advance that the server supports HTTP/2. In this case, you will configure the HttpClient object to use only HTTP/2 when sending requests to the server.

The server side

For this example, we need to find a web server that supports h2c. It’s not a common configuration for servers since no browser will be able to talk to the server, therefore I was not able to find an example website on the internet.

So, I created a test ASP.NET project and hosted it in Kestrel with HTTP/2 over clear text. In order to host the web API with HTTP/2 without TLS, you need to configure the ListenOptions.Protocols  property in the appsettings.js file:

"Kestrel": {
    "Endpoints": {
      "Http": {
        "Url": "http://localhost:5000",
        "Protocols": "Http2"
      },
      "Https": {
        "Url": "https://localhost:5001",
        "Protocols": "Http2"
      }
    }
}

In this case, my web app will support h2c on port 5000 and h2 on port 5001.

The client side

On the client side, I can run the following code to connect to the server with h2c:

static async Task Main(string[] args)
{
    HttpClient myHttpClient = new HttpClient
    {
        DefaultRequestVersion = HttpVersion.Version20,
        DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact
    };
    string requestUrl = "http://localhost:5000/swagger/index.html";

    try
    {
        Console.WriteLine($"GET {requestUrl}.");

        HttpResponseMessage response = await myHttpClient.GetAsync(requestUrl);
        response.EnsureSuccessStatusCode();

        Console.WriteLine($"Response HttpVersion: {response.Version}");

        string responseBody = await response.Content.ReadAsStringAsync();
        Console.WriteLine($"Response Body Length is: {responseBody.Length}");
        Console.WriteLine($"------------Response Body------------");
        Console.WriteLine(responseBody);
        Console.WriteLine($"------------End of Response Body------------");

    }
    catch (HttpRequestException e)
    {
        Console.WriteLine($"HttpRequestException : {e.Message}");
    }

    Console.WriteLine($"Press Enter to exit....");
    Console.ReadLine();
}

When you run this client app, it gives you the following output:

h2c connections test result

If you change the DefaultVersionPolicy to RequestVersionOrHigher, the result is still the same. But, if you change it to RequestVersionOrLower, the connection will not work because the client will automatically try to connect with HTTP/1.1, which is not supported by the server in our case.

h2c connection error

However, if the server supports both HTTP/1.1 and HTTP/2 over clear text and you set the policy to RequestVersionOrLower, then the system will end up using HTTP/1.1 anyway because the h2c connection upgrade feature is not supported in .NET 5.0 and .NET 6.0. The only use-case supported for h2c is: you have prior knowledge that the server supports HTTP/2 over clear text. This way you can simply configure the HttpClient object to use HTTP/2 protocol at first.

What if you are using HttpRequestMessage?

The DefaultRequestVersion and DefaultVersionPolicy options are available on the HttpClient object itself. These properties will be applied to all requests done through the regular HttpClient methods like GetAsync, PostAsync, PatchAsync, or PutAsync. If you use the Send or SendAsync method, where you have to pass an instance of HttpRequestMessage, you need to set the Version and VersionPolicy properties on the HttpRequestMessage instance. In such a case, these options apply only to that specific request instance.

Configuring HttpClient to use multiple connections with HTTP/2

The HTTP/2 standard recommends using only 1 TCP connection to communicate with the server. Here is what is specified in the standard:

HTTP/2 standard about connections

Therefore, in .NET 5.0 and .NET 6.0, the HttpClient is configured by default to open only 1 connection to an HTTP/2 server. In addition, the recommended value for the maximum number of concurrent streams is 100. Meaning, that the client should be able to handle 100 streams in the same TCP connection, which in most simple scenarios, equates to the maximum number of concurrent requests in the connection.

In some cases, for performance reasons, you might need more than 1 connection to the server if you are sending thousands of requests in a short time span. For that reason, in .NET 5.0, Microsoft introduces the EnableMultipleHttp2Connections property on the SocketsHttpHandler class to give the ability to developers to change the default behavior depending on their needs.

Here is an example of how to configure the HttpClient object to use multiple connections:

NB: For the purpose of this test, we are using the same ASP.NET server that we configured previously to support HTTP/2 over clear text.





static async Task Main(string[] args)
{
    var socketsHandler = new SocketsHttpHandler
    {
        EnableMultipleHttp2Connections = true
    };

    HttpClient myHttpClient = new HttpClient(socketsHandler)
    {
        DefaultRequestVersion = HttpVersion.Version20,
        DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact
    };
    string requestUrl = "http://localhost:5000/swagger/index.html";

    IEnumerable<Task> tasks = Enumerable.Range(1, 1000).Select(i => Task.Run(async () =>
    {
        try
        {
            Console.WriteLine($"GET {requestUrl}. Iteration: {i}");

            HttpResponseMessage response = await myHttpClient.GetAsync(requestUrl);
            response.EnsureSuccessStatusCode();

            Console.WriteLine($"Response HttpVersion for iteration {i}: {response.Version}");

            string responseBody = await response.Content.ReadAsStringAsync();
            Console.WriteLine($"Response Body Length for iteration {i} is: {responseBody.Length}");

        }
        catch (HttpRequestException e)
        {
            Console.WriteLine($"HttpRequestException : {e.Message}");
        }
    }));

    await Task.WhenAll(tasks);
    Console.WriteLine($"Press Enter to exit....");
    Console.ReadLine();
}

The execution of this code gives the following result (from the TcpView tool):

HttpClient HTTP/2 multiple connections tests

In this case, the client opened 9 connections to the server in order to send the 1000 requests simultaneously. Depending on the speed of execution, this could go up to a maximum of 10 connections, which equates to 1 connection per 100 requests. If you set the EnableMultipleHttp2Connections property to false, you will notice that the client opens only 1 TCP connection to send the 1000 HTTP requests. Depending on your needs, you can decide to enable this setting or not.

Summary

I hope this article gave you enough insights about how to configure the HttpClient object to use the HTTP/2 protocol. Basically, the HttpClient object uses HTTP/1.1 protocol by default in .NET 5.0 and .NET 6.0. Since many servers already support the HTTP/2 protocol, you surely need to configure your HttpClient instances to use HTTP/2, especially if you are sending requests with HTTPS. Your application could benefit from the performance improvements that come with using the HTTP/2 protocol.