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

In the previous post in my demystifying HttpClient series, I looked at the internals of HttpRequestMessage. I want to continue the series by investigating the HttpResponseMessage. This class is mostly a property holder, with little internal logic but there are a few behaviours and best practices to watch out for.

If you’ve read my previous post, you’ll notice that much of the information here is very similar. Frankly, this isn’t the most exciting post I’ve ever written, but if you do want to fully understand the HttpResponseMessage, I hope this will help!

Please note: Some of the code for HttpResponseMessage is marked internal, so the implementation is always subject to change. The information provided in this post is correct as at April 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 a HttpResponseMessage

Typically, you won’t need to construct an instance of HttpResponseMessage directly. A HttpResponseMessage will be returned to you by HttpClient as the result of making a request. You may have occasion to create one when you’re mocking responses during unit testing.

The main constructor for the HttpResponseMessage has the following signature:

public HttpResponseMessage(HttpStatusCode statusCode)

It accepts a HttpStatusCode enum which represents the HTTP status code received from the server. Some basic validation occurs here which ensures that once cast to an int, the status code value is between 0 and 999. The backing field _statusCode is set with the provided HttpStatusCode.

The value of the _version field is also initialised inside this constructor, using a default which comes from a property named DefaultResponseVersion on the static HttpUtilities class. This, in turn, calls the static HttpVersion class, accessing its Version11 field which creates a new Version instance…

public static readonly Version Version11 = new Version(1, 1);

A parameterless constructor also exists on HttpResponseMessage:

public HttpResponseMessage()

This constructor will call the main constructor using the default status code which is 200 – OK.

HttpResponseMessage Properties

There are several public properties on HttpResponseMessage which all expose a getter and setter.

The ‘Version’ property allows the HTTP message version for the response to be set. It uses the Version class as its type. When we looked at the construction of a HttpResponseMessage, we saw that the version defaulted to version 1.1.

The ‘StatusCode’ property allows the status code, which is an enum of type HttpStatusCode, to be set or retrieved. As we saw above, this is set when an instance of HttpResponseMessage is first constructed. If this is set outside of the constructor, through the property setter, a range check occurs to ensure the int value is between 0 and 999. The status code can be set internally by the HttpConnection and Http2Stream without this range check via the SetStatusCodeWithoutValidation method.

The ‘ReasonPhrase’ property is a string value which may be sent by servers in addition to the status code. The reason phrase can legally be null but cannot contain either the carriage return or line feed characters. Validation occurs in the property setter to ensure that there are no newline characters in the string. The HttpConnection can also set this without validation via the internal SetReasonPhraseWithoutValidation method.

The ‘Headers’ property is used to store any headers that have been sent as part of the response. The type for this property is HttpResponseHeaders which is complicated enough that it’s something I’d like to investigate in a future post (or posts).

A pull request has recently been merged and is due to be included as part of .NET Core 3.0 which adds a new property called ‘TrailingHeaders’ to HttpResponseMessage. This property supports the use of HTTP 1.x trailing headers on chunked responses. TrailingHeaders is also of type HttpResponseHeaders.

“A trailer allows the sender to include additional fields at the end of a chunked message to supply metadata that might be dynamically generated while the message body is sent, such as a message integrity check, digital signature, or post-processing status. The trailer fields are identical to header fields, except they are sent in a chunked trailer instead of the message’s header section.” – https://tools.ietf.org/html/rfc7230#section-4.1.2

The ‘RequestMessage’ property holds a reference to the HttpRequestMessage which was sent to the server and resulted in this response.

Finally, there is ‘Content’ property which is of type HttpContent and stores the content of the response. HttpContent is an abstract class of which there are various derived types. We’ll look in more detail at the HttpContent class in a future post.

Additional Methods

The HttpResponseMessage includes two convenience methods which can be used to check the status of the response received from the server. The first method IsSuccessStatusCode() can be called to check if the response was a success, based on its status code. If the status code value is between 200 and 299 (inclusive), then the response is considered a success and the returned boolean value will be true. Otherwise, it will be false.

The method called EnsureSuccessStatusCode() can also be used to check the status of the response. In this case, the method will throw a HttpRequestException if the status is not in the successful range (200-299). In cases where the response has a successful status code, this method not throw and will return a reference to the HttpResponseMessage (this).

Important Note: The behaviour of EnsureSuccessStatusCode has been changed in a pull request which will be included in the .NET Core 3.0 release. In .NET Framework and versions of .NET Core up to and including 2.2, the original behaviour applies. This original behaviour, in addition to throwing an exception when the response is not successful, will also dispose of the response content in cases where it’s not null. Also note it is only the content that gets disposed here, not the HttpResponseMessage itself. This original behaviour intended to free managed and unmanaged resources in a non-success scenario.

As of .NET Core 3.0, the content will no longer be disposed when an exception is thrown. The reason specified for this change is that this is a non-standard behaviour which you may not expect as a side effect of calling the EnsureSuccessStatusCode method. If you are reliant on this behaviour and upgrade to .NET Core 3.0, you will need to handle disposal when an exception is thrown.

The HttpResponseMessage also includes an overloaded ToString method which returns a formatted string containing details of the response, including the response headers. Here is an example of the format of that string:

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

The implementation details of HttpResponseMessage and the mechanism for reading content into it from the underlying Socket means that specific guidance on when you may need to dispose of a HttpResponseMessage becomes quite nuanced.

In the majority of cases, the body of the response from the underlying connection (Socket) is fully received, and the byte data, representing the content will be buffered into a memory stream automatically. This occurs when using most overloads of the HttpClient APIs (GetAsync, PostAsync and SendAsync).

Once data has finished buffering, the underlying connection which was used to make the request will go idle and be marked as having completed. It will also be disassociated from the HttpContentStream and therefore be available to handle further requests via the HttpClient.

When calling dispose manually on HttpResponseMessage, in the above case, since the connection is already released the call to Dispose will merely dispose of the HttpContent MemoryStream. This isn’t necessary for its current implementation and can be safely skipped.

Note that in cases where you choose to only receive the headers by using an overload on the HttpClient methods using “HttpCompletionOption.ResponseHeadersRead”; the content will not be automatically buffered. As a result, the connection will remain in use at this point and be tied up until you free it. Therefore, it’s a best practice to dispose of the content after you have read the content, to ensure that the connection is released and returned to the pool for the next request.

The safest, general advice would be to always dispose of the HttpResponseMessage once you have finished with using it. This does lead to a little more code noise but ensures that regardless of the internals and any future changes, your code will free/clean up unused resources such as connections as quickly as possible. Be aware that once you dispose of the content, it will become unusable for anyone else who has been passed a reference to it.

I’ll aim to cover the further implementation details of disposing of things like HttpContent in some future posts.

Best Practices

When a HttpResponseMessage is returned to you from a request sent via HttpClient, you should never assume that everything succeeded. It’s a best practice to validate that the response status code is in the success range before trying to consume the content. You should choose between the use of EnsureSuccessStatusCode, which throws when the response is not successful and using the IsSuccessStatusCode convenience method to check for success/failure.

If you’re happy to throw an exception and let your caller catch it / deal with it, EnsureSuccessStatusCode is a convenient option. If you prefer to handle failure cases more explicitly, you can use the IsSuccessStatusCode method to check for success/failure and then act accordingly. You may want to do some logging and then return a default response for example.

The following code snippet is one example of making a request using HttpClient. The focus of this sample is on best practice consumption of the HttpResponseMessage:

The key lines are line 11, which shows the use of EnsureSuccessStatusCode and the finally block starting at line 15. Using try/finally here ensures that regardless of exceptions being thrown above, we ensure that we manually dispose of the response (and underlying content stream). In the rare cases where the response content stream had not been fully buffered, this also ensures that we release the underlying connection for future requests.

You could simplify this code if you prefer with a using block:

Using ResponseHeadersRead

If you make requests with the HttpCompletionOption.ResponseHeadersRead then you should always be sure to dispose of the response to ensure that the underlying connection is released:

Summary

Most of this post has been quite similar to my previous post about HttpRequestMessage. For most cases, you’ll be a consumer of the HttpResponseMessage and access properties on it to first check that a request was successful, then consume the headers and/or content that you need from it.

Acknowledgements

I want to extend a big thank you to David Shulman from the .NET team for spending time to review some of my initial code analysis, specifically around the disposal of HttpResponseMessage.


Have you enjoyed this post and found it useful? If so, please consider supporting me:

Buy me a coffeeBuy me a coffee Donate with PayPal

Steve Gordon

Steve Gordon is a Pluralsight author, 6x Microsoft MVP, and a .NET engineer at Elastic where he maintains the .NET APM agent and related libraries. Steve is passionate about community and all things .NET related, having worked with ASP.NET for over 21 years. Steve enjoys sharing his knowledge through his blog, in videos and by presenting talks at user groups and conferences. Steve is excited to participate in the active .NET community and founded .NET South East, a .NET Meetup group based in Brighton. He enjoys contributing to and maintaining OSS projects. You can find Steve on most social media platforms as @stevejgordon

Leave a Reply

Your email address will not be published. Required fields are marked *