HttpClient - Designing Evolvable Web APIs with ASP.NET (2012)

Designing Evolvable Web APIs with ASP.NET (2012)

Chapter 14. HttpClient

It is always easier to get a good result with good tools.

This chapter is a deeper exploration of the HttpClient library that is part of the System.Net.Http library discussed in Chapter 10.

The first incarnation of HttpClient was bundled with the REST Starter Kit (RSK) on CodePlex in early 2009. It introduced a number of concepts such as a request/response pipeline, an abstraction for the HTTP payload that was distinct from the request/response and strongly typed headers. Despite a big chunk of RSK making it into .NET Framework 4.0, HttpClient itself did not. When the Web API project started in 2010, a rewritten version of HttpClient was a core part of the project.

HttpClient Class

Simple things should be simple, and HttpClient tries to adhere to that principle. Consider the following:

var client = new HttpClient();

string rfc2616Text =

await client.GetStringAsync("http://www.ietf.org/rfc/rfc2616.txt");

In this example, a new HttpClient object is instantiated and an HTTP GET request is made, and the content of the response is translated to a .NET string.

This apparently trivial piece of code provides sufficient context for us to discuss a range of issues related to the usage of the HttpClient class.

Lifecycle

Although HttpClient does indirectly implement the IDisposable interface, the recommended usage of HttpClient is not to dispose of it after every request. The HttpClient object is intended to live for as long as your application needs to make HTTP requests. Having an object exist across multiple requests enables a place for setting DefaultRequestHeaders and prevents you from having to respecify things like CredentialCache and CookieContainer on every request, as was necessary with HttpWebRequest.

Wrapper

Interestingly, HttpClient itself does not do any of the dirty work of making HTTP requests; it defers that job to an aggregated object that derives from HttpMessageHandler. The default constructor takes care of instantiating one of these objects. Alternatively, one can be passed in to the constructor, like this:

var client = new HttpClient((HttpMessageHandler) new HttpClientHandler());

HttpClientHandler uses the System.Net HttpWebRequest and HttpWebResponse classes under the covers. This design provides the best possible outcome. Today, we get a clean new interface to a proven HTTP stack, and tomorrow we can replace that HttpClientHandler with some improved HTTP internals and the application interface will not change. The implementation of HttpClientHandler uses a lowest common denominator of the System.Net library to allow usage on multiple platforms like WinRT and Windows Phone. As a result, certain features are not available, such as client caching, pipelining, and client certificates, as they are dependent on the desktop operating system. To use those features, it is necessary to do the following:

var handler = new WebRequestHandler {

AuthenticationLevel = AuthenticationLevel.MutualAuthRequired,

CachePolicy = new RequestCachePolicy(RequestCacheLevel.Default)

};

var httpClient = new HttpClient(handler);

The WebRequestHandler class derives from HttpClientHandler but is deployed in a separate assembly, System.Net.Http.WebRequest.

The reason that HttpClient implements IDisposable is to dispose the HttpMessageHandler, which then attempts to close the underlying TCP/IP connection. This means that creating a new HttpClient and making a new request will require creating a new underlying socket connection, which is very expensive in comparison to just making a request.

It is important to realize that if an instance of the HttpMessageHandler class is instantiated outside of the HttpClient and then passed to the constructor, disposing of the HttpClient will render the handler unusable. If there is significant setup required to configure the handler, you might wish to be able to reuse a handler across multiple HttpClient instances. Fortunately, an additional constructor was added to support this scenario:

var handler = HttpHandlerFactory.CreateExpensiveHandler(};

var httpClient = new HttpClient(handler, disposeHandler:false);

Instructing HttpClient to not dispose the HttpMessageHandler allows the reuse of the message handler across multiple HttpClient instances.

Multiple Instances

One reason that you might want to create multiple HttpClient instances is because certain properties of HttpClient cannot be changed once the first request has been made. These include:

public Uri BaseAddress

public TimeSpan Timeout

public long MaxResponseContentBufferSize

Thread Safety

HttpClient is a threadsafe class and can happily manage multiple parallel HTTP requests. If these properties were to be changed while requests were in progress, then it might introduce bugs that are hard to track down.

These properties are relatively self-explanatory, but the MaxResponseContentBufferSize is worth highlighting. This property is of type long but is defaulted and limited to the Int32.MaxValue, which is sufficiently large to start with for most scenarios. Fear not, though: just because the size is set to 4GB, HttpClient will not allocate more memory than is required to buffer the HTTP payload.

Beyond being a wrapper for the HttpMessageHandler, a host for configuration properties, and a place for some logging messages, HttpClient also provides some helper methods to make issuing common requests easy. These methods make HttpClient a good replacement forSystem.Net.WebClient.

Helper Methods

The first example introduced GetStringAsync. Additionally, there are GetStreamAsync and GetByteArrayAsync methods. All of the helper methods have the Async suffix to indicate that they will execute asynchronously, and they all return a Task object. This will also allow the use of async and await on platforms that support those keywords. In .NET 4.5, it has become policy to expose methods that could take longer than 50 milliseconds as asynchronous only. This policy is intended to encourage developers to take approaches that will not block an application’s user interface thread and therefore create a more responsive application. Our example used the .Result property on the returned task to block the calling thread and return the string result. This approach circumvents the recommended policy and comes with some dangers that will be addressed later in this chapter. However, for simplicity’s sake, we’ll use the .Result shortcut to simulate a synchronous request.

Peeling Off the Layers

The helper methods are handy but provide little insight into what is going on under the covers. If we remove one layer of simplification we have the GetAsync method, which can be used as follows:

var client = new HttpClient();

HttpResponseMessage response;

response = await client.GetAsync("http://www.ietf.org/rfc/rfc2616.txt");

HttpContent content = response.Content;

string rfc2616Text = await content.ReadAsStringAsync();

In this example, we now get access to the response object and the content object. These objects allow us to inspect metadata in the HTTP headers to guide the process of consuming the returned content.

Completed Requests Don’t Throw

The HttpClient behavior is different than HttpWebRequest in that, by default, no completed HTTP responses will throw an exception. An exception may be thrown if something fails at the transport level; however, unlike HttpWebRequest, status codes like 3XX, 4XX, and 5XX do not throw exceptions. The IsSuccessStatusCode property can be used to determine if the status code is a 2xx, and the EnsureSuccessStatusCode method can be used to manually trigger an exception to be thrown if the status code is not successful.

The status codes returned in response to an HTTP request often can be handled directly by application code and therefore do not warrant throwing an exception. For example, we can handle many 3xx responses automatically by making a second request to the URI specified in the location header. Error 503 Service Unavailable can apply a retry mechanism to ensure temporary interruptions are not fatal to the application. Later in this chapter, there will be further discussion about building clients that intelligently react to HTTP status codes.

Content Is Everything

The HttpContent class is an abstract base class that comes with a few implementations in the box. HttpContent abstracts away the details of dealing with the bytes that need to be sent over the wire. HttpContent instances deal with the headaches of flushing and positioning streams, allocating and deallocating memory, and converting CLR types to bytes on the wire. They provide access to the HTTP headers that are specifically related to the HTTP payload.

You can access the content of an HttpContent object using the ReadAs methods discussed in Chapter 10. Although the HttpContent abstraction does shield you from most of the nasty details about reading bytes over the wire, there is one key detail that is not immediately apparent and is worth understanding. A number of the methods on HttpClient have a completionOption parameter. This parameter determines whether the asynchronous task will complete as soon as the response headers have been received or whether the complete response body will be completely read into a buffer first.

There are a couple reasons why you might want to have the task complete as soon as the headers are retrieved:

§ The media type of the response may not be understood by the client, and where networks are metered, downloading the bytes may be a waste of time and money.

§ You may wish to do processing work based on the response headers in parallel to downloading the content.

The following code is a hypothetical example of how this feature could be used:

var httpClient = new HttpClient();

httpClient.BaseAddress = new Uri("http://www.ietf.org/rfc/");

var tcs = new CancellationTokenSource();

var response = await httpClient.GetAsync("rfc2616.txt",

HttpCompletionOption.ResponseHeadersRead, tcs.Token);

// Headers have been returned

if (!IsSupported(response.Content.ContentType)) {

tcs.Cancel();

return;

}

UIManager userInterfaceManager = new UIManager();

// Start building up the right UI based on the content-type

userInterfaceManager.PrepareTheUI(content.ContentType);

// Start pulling the payload data across the wire

var payload = await response.Content.ReadAsStreamAsync()

// Payload has been completely retrieved

userInterfaceManager.Display(payload);

To implement this technique, the UIManager has to do some thread synchronization because the Display method will likely be called on a different thread than PrepareTheUI, and the Display method will probably need to wait until the UI is ready. Sometimes the extra effort is worth the performance gain of being able to effectively do two things at once. Obviously, this technique is not much use if your client can’t determine what you are trying to display without parsing the payload.

Cancelling the Request

The last parameter to discuss on the GetAsync method is the CancellationToken. Creating a CancellationToken and passing it to this method allows calling objects the opportunity to cancel the Async operation. Be aware that cancelling an operation will cause the Async operation to throw an exception, so be prepared to catch it.

The following example cancels a request if it does not complete within one second. This illustrates the use of Cancel only, as HttpClient has a built-in timeout mechanism:

[Fact]

public async Task RequestCancelledByCaller()

{

Exception expectedException = null;

bool done = false;

var httpClient = new HttpClient();

var cts = new CancellationTokenSource();

var backgroundRequest = new TaskFactory().StartNew(async () =>

{

try

{

var request = new HttpRequestMessage()

{

RequestUri = new Uri("http://example.org/largeResource")

};

var response = await httpClient.SendAsync(request,

HttpCompletionOption.ResponseHeadersRead, cts.Token);

done = true;

}

catch (TaskCanceledException ex)

{

expectedException = ex;

}

}, cts.Token);

// Wait for it to finish

Thread.Sleep(1000);

if (!done)

cts.Cancel();

Assert.NotNull(expectedException);

}

SendAsync

All of the HttpClient methods we have covered so far are just wrappers around the single method SendAsync, which has the following signature:

public Task<HttpResponseMessage> SendAsync(

HttpRequestMessage request,

HttpCompletionOption completionOption,

CancellationToken cancellationToken)

By creating an HttpRequestMessage and setting the Method property and the Content property, you can easily replicate the behavior of the helper methods using SendAsync. However, the HttpRequestMessage can be used only once. After the request is sent, it is disposed immediately to ensure that any associated Content object is disposed. In many cases this shouldn’t be necessary; however, if an HttpContent object were wrapping a forward-only stream it would not be possible to resend the content without reinitializing the stream, and the HttpContentclass does not have any such interface. Introducing a link class as a request factory (as we did in Chapter 9) is one way to work around this limitation:

var httpClient = new HttpClient();

httpClient.BaseAddress = new Uri("http://www.ietf.org/rfc/");

var request = new HttpRequestMessage() {

RequestUri = new Uri("rfc2616.txt"),

Method = HttpMethod.Get

}

var response = await httpClient.SendAsync(request,

HttpCompletionOption.ResponseContentRead, new CancellationToken());

The SendAsync method is a core piece of the architecture of both HttpClient and Web API. SendAsync is the primary method of HttpMessageHandler, which is the building block of request and response pipelines.

Client Message Handlers

Message handlers are one of the key architectural components in both HttpClient and Web API. Every request and response message, on both client and server, is passed through a chain of classes that derive from HttpMessageHandler. In the case of HttpClient, by default there is only one handler in the chain: the HttpClientHandler. You can extend the default behavior by inserting additional HttpMessageHandler instances at the begining of the chain, as Example 14-1 demonstrates.

Example 14-1. Adding a handler to the client request pipeline

var customHandler = new MyCustomHandler()

{ InnerHandler = new HttpClientHandler()};

var client = new HttpClient(customHandler);

client.GetAsync("http://example.org",content);

The code in Example 14-1 creates an object graph that looks like Figure 14-1.

Extensibility with HttpMessageHandlers

Figure 14-1. Extensibility with HttpMessageHandlers

Multiple message handlers can be chained together to compose additional functionality. However, the base HttpMessageHandler does not have a built-in chaining capability. A derived class, DelegatingHandler, provides the InnerHandler property to support chaining.

Example 14-2 shows how you can use a message handler to allow a client to use the PUT and DELETE methods against a server that does not support those methods and requires the use of the X-HTTP-Method-Override header.

Example 14-2. HttpMethodOverrideHandler

public class HttpMethodOverrideHandler: DelegatingHandler

{

protected override Task<HttpResponseMessage> SendAsync(

HttpRequestMessage request,

System.Threading.CancellationToken cancellationToken)

{

if (request.Method == HttpMethod.Put)

{

request.Method = HttpMethod.Post;

request.Headers.Add("X-HTTP-Method-Override", "PUT");

}

if (request.Method == HttpMethod.Delete)

{

request.Method = HttpMethod.Post;

request.Headers.Add("X-HTTP-Method-Override", "DELETE");

}

return base.SendAsync(request, cancellationToken);

}

}

Another class derived from DelegatingHandler, called MessageProcessingHandler (see Example 14-3), makes it even easier to create these handlers as long as the custom behavior will not need to do any long-running work that would require asynchronous operations.

Example 14-3. MessageProcessingHandler

public class HttpMethodOverrideMessageProcessor : MessageProcessingHandler {

protected override HttpRequestMessage ProcessRequest(

HttpRequestMessage request,

CancellationToken cancellationToken) {

if (request.Method == HttpMethod.Put)

{

request.Method = HttpMethod.Post;

request.Headers.Add("X-HTTP-Method-Override", "PUT");

}

if (request.Method == HttpMethod.Delete)

{

request.Method = HttpMethod.Post;

request.Headers.Add("X-HTTP-Method-Override", "DELETE");

}

return request;

}

protected override HttpResponseMessage ProcessResponse(

HttpResponseMessage response,

CancellationToken cancellationToken) {

return response;

}

}

When using these message handlers to extend functionality, you should be aware that they will often execute on a different thread than the thread that issued the request. If the handler attempts to switch back onto the requesting thread—for example, to get back onto the UI thread to update some user interface control—then there is a risk of deadlock. If the original request is blocking, waiting for the response to return, then a deadlock will occur. You can avoid this problem when using the .NET 4.5 async await mechanism, but it is a very good reason to avoid using .Resultto simulate synchronous requests.

Proxying Handlers

There are many potential uses of HttpMessageHandlers. One is to act as a proxy for manipulating outgoing requests. The following example shows a proxy for the Runscope debugging service:

public class RunscopeMessageHandler : DelegatingHandler

{

private readonly string _bucketKey;

public RunscopeMessageHandler(string bucketKey,

HttpMessageHandler innerHandler)

{

_bucketKey = bucketKey;

InnerHandler = innerHandler;

}

protected override Task<HttpResponseMessage> SendAsync(

HttpRequestMessage request,

CancellationToken cancellationToken)

{

var requestUri = request.RequestUri;

var port = requestUri.Port;

request.RequestUri = ProxifyUri(requestUri, _bucketKey);

if ((requestUri.Scheme == "http" && port != 80 )

|| requestUri.Scheme == "https" && port != 443)

{

request.Headers.TryAddWithoutValidation(

"Runscope-Request-Port", port.ToString());

}

return base.SendAsync(request, cancellationToken);

}

private Uri ProxifyUri(Uri requestUri,

string bucketKey,

string gatewayHost = "runscope.net")

{

...

}

}

In this scenario, the request URI is modified to point to the proxy instead of the original resource.

Fake Response Handlers

You can use message handlers to assist with testing client code. If you create a message handler that looks like the following:

public class FakeResponseHandler : DelegatingHandler

{

private readonly Dictionary<Uri, HttpResponseMessage> _FakeResponses

= new Dictionary<Uri, HttpResponseMessage>();

public void AddFakeResponse(Uri uri, HttpResponseMessage responseMessage)

{

_FakeResponses.Add(uri,responseMessage);

}

protected async override Task<HttpResponseMessage> SendAsync(

HttpRequestMessage request,

CancellationToken cancellationToken)

{

if (_FakeResponses.ContainsKey(request.RequestUri))

{

return _FakeResponses[request.RequestUri];

}

else

{

return new HttpResponseMessage(HttpStatusCode.NotFound)

{ RequestMessage = request};

}

}

}

you can use it as a replacement for the HttpClientHandler:

[Fact]

public async Task CallFakeRequest()

{

var fakeResponseHandler = new FakeResponseHandler();

fakeResponseHandler.AddFakeResponse(

new Uri("http://example.org/test"),

new HttpResponseMessage(HttpStatusCode.OK));

var httpClient = new HttpClient(fakeResponseHandler);

var response1 = await httpClient.GetAsync("http://example.org/notthere");

var response2 = await httpClient.GetAsync("http://example.org/test");

Assert.Equal(response1.StatusCode,HttpStatusCode.NotFound);

Assert.Equal(response2.StatusCode, HttpStatusCode.OK);

}

In order to test client-side services, you must ensure that they allow an HttpClient instance to be injected. This is another example of why it is better to share an HttpClient instance rather than instantiating on the fly, per request. The FakeResponseHandler needs to be prepopulated with the responses that are expected to come over the wire. This setup allows the client code to be tested as if it were connected to a live server:

[Fact]

public async Task ServiceUnderTest()

{

var fakeResponseHandler = new FakeResponseHandler();

fakeResponseHandler.AddFakeResponse(

new Uri("http://example.org/test"),

new HttpResponseMessage(HttpStatusCode.OK)

{Content = new StringContent("99")});

var httpClient = new HttpClient(fakeResponseHandler);

var service = new ServiceUnderTest(httpClient);

var value = await service.GetTestValue();

Assert.Equal(value, 99);

}

Creating Resuable Response Handlers

In Chapter 9, we discussed the notion of reactive clients that decoupled the response handling from the context of the request.

Message handlers and the HttpClient pipeline are a natural fix for achieving this goal, and have the side effect of simplifying the process of making requests.

Consider the message handler shown in Example 14-4.

Example 14-4. Pluggable response handler

public abstract class ResponseAction

{

abstract public bool ShouldRespond(

ClientState state,

HttpResponseMessage response);

abstract public HttpResponseMessage HandleResponse(

ClientState state,

HttpResponseMessage response);

}

public class ResponseHandler : DelegatingHandler

{

private static readonly List<ResponseAction> _responseActions

= new List<ResponseAction>();

public void AddResponseAction(ResponseAction action)

{

_responseActions.Add(action);

}

protected override Task<HttpResponseMessage> SendAsync(

HttpRequestMessage request,

CancellationToken cancellationToken)

{

return base.SendAsync(request, cancellationToken)

.ContinueWith<HttpResponseMessage>(t =>

ApplyResponseHandler(t.Result));

}

private HttpResponseMessage ApplyResponseHandler(

HttpResponseMessage response)

{

foreach (var responseAction in _responseActions)

{

if (responseAction.ShouldRespond(response))

{

var response = responseAction.HandleResponse(response);

if (response == null) break;

}

}

return response;

}

}

In this example, we have created a delegating handler that will dispatch responses to a particular ResponseAction class if ShouldRespond returns true. This mechanism allows an arbitrary number of response actions to be defined and plugged in. The ShouldRespond method’s role can be as simple as looking at the HTTP status code, or it could be far more sophisticated, looking at content type or even parsing the payload looking for specific tokens.

Making HTTP requests then gets simplified to what you see in Example 14-5.

Example 14-5. Using response handlers

var responseHandler = new ResponseHandler()

{InnerHandler = new HttpClientHandler()};

responseHandler.AddAction(new NotFoundHandler());

responseHandler.AddAction(new BadRequestHandler());

responseHandler.AddAction(new ServiceUnavailableRetryHandler());

responseHandler.AddAction(new ContactRenderingHandler());

var httpClient = new HttpClient(responseHandler);

httpClient.GetAsync("http://example.org/contacts");

Conclusion

HttpClient is a major step forward in the use of HTTP on the .NET platform. We get an interface that is as easy to use as WebClient but with more power and configurability than HttpWebRequest/HttpWebResponse. The same interface will support future protocol implementations. Testing is easier, and the pipeline architecture allows us to apply many cross-cutting concerns without complicating the usage.