Demystifying HttpClient Internals: HttpRequestMessage A look at the internals of HttpRequestMessage

Anyone who regularly works with HttpClient will have likely have also used the HttpRequestMessage class. While HttpClient supports some convenience methods for quick actions such as sending a GET request; once you have more complex requirements, such a needing to include headers on the request, you will need to use HttpClient.SendAsync which accepts a HttpRequestMessage.

HttpRequestMessage is a pretty simple class which is mostly a property bag for request data. Down the stack, this data will be turned into the raw bytes which form a correctly formatted HTTP message.

I went back and forth on whether there was even value in writing a blog post about this specifically. After a little bit of pondering, I decided, it’ll be a short post and perhaps it will still be interesting and useful for some people. As I then sat down to explain things, I realised that there’s a little more depth to a few elements than I first appreciated.

Please note: Some of the code for HttpRequestMessage is marked internal, so it is always subject to change. This information is correct as at March 2019 against the latest merged code under the master branch of corefx. That said, I wouldn’t expect major parts of this to change too dramatically. You can always review the code yourself in the corefx repo.

Creating an Instance of HttpRequestMessage

HttpRequestMessage supports three constructors:

public HttpRequestMessage(HttpMethod method, Uri requestUri)

This constructor accepts a HttpMethod and a Uri for the request. These values are passed to the private InitializeValues method. The HttpMethod cannot be null. The Uri on the other can be, since this request may be sent via a HttpClient which has a base address set. In a case where the HttpRequestMessage has a null (or empty, which is treated as equivalent) Uri and the HttpClient does not have a BaseAddress set, the send will throw at that point.

public HttpRequestMessage(HttpMethod method, string requestUri)

This constructor is very similar to the previous one. The only difference is that it accepts a string for the Uri as its second parameter. It creates a new Uri using the requestUri string parameter with a UriKind of RelativeOrAbsolute.

HttpRequestMessage()

The default constructor accepts no parameters and results in a new HttpRequestMessage with the HttpMethod set as GET and the Uri set as null.

Public Properties

The HttpRequestMessage class has six public properties exposed on it which can all be used to get or set the value. Let’s step through those…

Method is fairly self-explanatory; It defines the HTTP method (HttpMethod type) for the request which will be performed against the remote resource. Typically you set this using one of the static properties on HttpMethod which cover the common methods such as GET, POST, DELETE etc. As we’ve seen above, on a default HttpRequestMessage the request will be a GET request.

RequestUri can be used to set the Uri for the request. This may be an absolute URI or in cases where the HttpClient.BaseAddress is set, a relative one will also be valid here. The Uri can also be null which will only work in situations where a client BaseAddress is set.

Headers is used to apply request specific headers. The type for this property is HttpRequestHeaders which is complex enough that it’s something I’d like to investigate in a future post (or posts). You can set certain predefined headers or you can call the Add method to add custom ones by name and value. When you set headers on a request directly, they take precedence over values of any DefaultRequestHeaders set on the HttpClient used to send the request.

The Version property allows the HTTP message version for the request to be set. It uses the Version class as its type. When you create a new HttpRequestMessage via any of the constructor overloads, they all call into a private method which initialises its values. The version value is initialised using a default which comes from a property named DefaultRequestVersion on the static HttpUtilities class. This in turn calls the static HttpVersion class, accessing its Version20 field which creates a new Version instance…

public static readonly Version Version20 = new Version(2, 0);

If you’re using .NET Core 2.1 or later, a new HttpRequestMessage defaults to Version 2.0. Prior to .NET Core 2.1 it’ll be 1.1.

The Properties property is an IDictionary<string, object> which can be used to store arbitrary data against a particular request. This can be useful to flow some context with the request which may for example be inspected by the handlers in order to apply some conditional logic when sending the request. I have a separate post planned to demonstrate how you may choose to make use of HttpRequestMessage.Properties.

Finally, we have Content which is of type HttpContent. This lets us set the content body for the request which you will include when sending a POST request for example. HttpContent is an abstract class of which there are various different derived types. For example, you can include string content for simple requirements. HttpContent is likely worth it’s own blog post as well.

Implementation Details

An interesting and important implementation detail of HttpRequestMessage is that it uses a field to track whether the message has been sent or not. This is updated internally by calling its MarkAsSent method. This uses Interlocked.Exchange to update the _sendStatus field. This fields holds an integer, where 0 represents not yet sent and 1 represents sent. The MarkAsSent method returns a bool indicating that the original value was MessageNotYetSent (zero).

This implementation detail means that you cannot reuse an instance of HttpRequestMessage to send multiple requests by passing the same instance repeatedly to HttpClient.SendAsync. Attempting to do so will result in an InvalidOperationException being thrown because HttpClient expects MarkAsSent to return true, indicating that the request has not previously been marked as sent.

In order to wrap the sending of a request with retry logic, you should create and use a MessageHandler which is used by HttpClient and therefore the MarkAsSent check is not repeated at this level.

ToString

HttpRequestMessage overrides ToString so that you can easily dump out the important details about the request message. It uses a StringBuilder to construct the final string which results in output which looks a like this…

To Dispose Or Not to Dispose, That is the Question!

Calling dispose on HttpRequestMessage only has any effect in certain specific situations. In others it may be an no-op. As with HttpClient, it is therefore hard to give singular advice which is correct in all scenarios. Here is the Dispose method…

The comment gives a hint to the nuance around the need to call Dispose. It’s applicable if the Content property has a HttpContent type which requires disposal. Of the currently available content types, only StreamContent may require disposal, and then only if the stream itself is a stream which needs to be disposed. If your stream is a MemoryStream for example, then disposal is not necessary (currently).

Therefore, if your code owns the creation of the HttpRequestMessage and sets the Content, you can probably make an appropriate decision based on the HttpContent you are using. If you’re using StringContent for example, there is no value in disposing of the request message (at least as it’s implemented today)!

If you’re less sure about the content which a request message includes, it totally valid and safer to err on the side of caution and in that case, dispose of the HttpRequestMessage when you’re successfully sent it via HttpClient.

Summary

I think that pretty much concludes the details of HttpRequestMessage which may be useful and valuable to know. This is not a particularly complex type, and the properties are well documented. It may be less obvious that a message can be sent only once via a HttpClient and that’s something worth bearing in mind. In terms of disposal, there’s no harm in disposing of this once you have used it, but in many cases this may not be necessary and could be unduly cluttering your code.