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 for System.Net.WebClient
.
Helper Methods
The first example introduced GetStringAsync
. Additionally, there are GetStream
Async
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 HttpContent
class 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.
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.
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.
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.
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 .Result
to 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.
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.
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.
Get Designing Evolvable Web APIs with ASP.NET now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.