Joel Verhagen

a computer programming blog

Adding More Control to HttpClient Redirects

I’m a big fan of the .NET Framework. It’s insane how much you can do without ever referencing a library outside of the base framework. One of my favorite additions to .NET 4.5 was the HttpClient class… and pretty much everything else in the System.Net.Http and System.Net.Http.WebRequest assemblies! Let’s just move past the days of WebClient and just forget it ever happened, hmkay? Now we can async all the things!

Motivation

I do have one small gripe about the default behavior of HttpClient: the way they implemented HTTP redirects. Yes, yes, they work great. All of options to enable, disable, and limit the redirects are available on the HttpClientHandler or WebRequestHandler. La-de-da. Unfortunately, they put all of the logic of redirection at the lowest handler level (actually they put the code in HttpWebRequest, but you’re not meant to reference this class directly). Wait, what’s wrong with that?

Well, first let’s loop around and talk about another great part of System.Net.Http: delegating handlers (DelegatingHandler). These guys allow you to compose a pipeline of operations you’d like to perform on an HTTP request on its way out or an HTTP response on its way in. Custom caching, authentication, logging, special content-type handling, mutate headers, etc. The possibilities are limitless!

What’s even better is this delegating handler stuff can be used on both the server side (with ASP.NET MVC and Web API) and client side. If you’re interested in reading more about delegating handlers, check out K. Scott Allen’s blog post about ‘em.

Now imagine you have a URL the leads through one, two, or seven redirects. Also imagine you have a delegating handler set up in your HttpClient that does some super helpful, super important logging:

HttpMessageHandler handler = new EpicLoggingDelegatingHandler
{
    InnerHandler = new WebRequestHandler()
};

var httpClient = new HttpClient(handler);

string url = "http://httpbin.org/redirect/5";
HttpResponseMessage response = httpClient.GetAsync(url).Result;
string content = response.Content.ReadAsStringAsync().Result;
Console.WriteLine(content);

As mentioned before, the HTTP redirect logic is done at the very lowest C# level. What this means is that a delegating handler you have configured into your HTTP client will only see the very first HttpRequestMessage and the very last HttpResponseMessage… and the last HttpRequestMessage with myResponse.RequestMessage. None of the messages in between.

Ideally, the redirect would be resolved in a delegating handler. Yes, this would make the most common case (no delegating handlers and the need for browser-like redirects) a little more complicated to wire-up, but the flexibility that comes with wrapping this bit of logic up in a delegating handler has benefits. This would even give a nicely encapsulated means for resolving interesting problems like HTTP 300 Multiple Choices :).

Imagine taking it a step forward and splitting out even more tricky logic currently handled by HttpWebRequest and putting it in different delegating handlers. Mix, match, compose. Interesting…

Code

Well, I focused on the problem at hand and implemented a delegating handler that overrides the built-in redirects, allowing you to put redirection anywhere you want in your client pipeline.

For now the code is in a GitHub Gist. If I find myself needing it in a lot of different projects or some of my readers would like it in a more accessible form, I could consider wrapping it up in a NuGet package.

Oh, and by default RedirectingHandler disables redirection in your inner HttpClientHandler. There are some other options on the handler that are worth looking at. Also, mad props to Kenneth Reitz’s Python requests library, which I ported for the redirect rules.

Example

HttpMessageHandler handler = new RedirectingHandler
{
    InnerHandler = new WebRequestHandler()
};

var httpClient = new HttpClient(handler);

string url = "http://httpbin.org/redirect/5";
HttpResponseMessage response = httpClient.GetAsync(url).Result;
string content = response.Content.ReadAsStringAsync().Result;
Console.WriteLine(content);

Have fun!