Using .NET's HttpClient without following redirects

By João Antunes

- 5 minutes read - 904 words
Using .NET's HttpClient without following redirects

I’m pretty sure (or maybe I’m wrong) that we seldom need to avoid following redirects, but I came across such a need one or two times, so I might as well write about it :)

Accompanying source code here.


So, for a concrete scenario where I actually needed the HttpClient not to follow redirects.

At work we were implementing a new feature that was dependant on a legacy system. This feature consisted in showing some banners (similar to ads) that could be clicked. When a banner is clicked it has to do a bunch of things:

  • Open a modal window to do some stuff
  • Ping a tracking url indicating that the banner was clicked
  • The tracking url may or may not do a redirect, in which case a new tab should be open

Let’s disregard the usability of this… those were the requirements.

So for the above steps, the problematic one was the third one - the legacy system we were integrating with is the one responsible for the banners and tracking urls, so we had to make it work with what we had. The first idea that came to mind was, well, always open a new tab, if it redirects then we’re good, if not… we have an open blank tab :)

Blank tabs aren’t very cool so we thought we could do better. After some more or less complicated ideas, the new plan was to create an indirection1 that would make the request to the tracking url without following the redirect - if the response was a 200, we’re done, if it was a 302 this middleman would return the target url and the application could open it in a new tab.


To do this in .NET we’re using as usual an HttpClient, but as its default behavior is to follow redirects, a little configuration was required.

 1var handler = new HttpClientHandler()
 3    AllowAutoRedirect = false
 5var httpClient = new HttpClient(handler);
 7var response = await _httpClient.GetAsync(trackingUrl, ct);
 9var targetUrl = response.StatusCode == HttpStatusCode.Redirect
10        ? response.Headers.Location.OriginalString
11        : null;

And that’s it! If you’re reading the article just to know the required configuration, this is it - so many words to end with about ten of lines of code… sorry :)

Now for the complete context, we’re doing this in a ASP.NET application, and by now one should be aware that we can’t go around instantiating http clients everywhere or we’ll end up with port exhaustion, so we ended up creating a class to abstract this.

 3namespace CodingMilitia.HttpClientNotFollowingRedirectsSample.Web
 5    public class TrackingHttpClientWrapper
 6    {
 7        private HttpClient _httpClient;
 9        public TrackingHttpClientWrapper()
10        {
11            var handler = new HttpClientHandler()
12            {
13                AllowAutoRedirect = false
14            };
15            _httpClient = new HttpClient(handler);
16        }
18        public async Task<string> TrackAsync(string trackingUrl, CancellationToken ct)
19        {
20            var response = await _httpClient.GetAsync(trackingUrl, ct);
22            return response.StatusCode == HttpStatusCode.Redirect
23                 ? response.Headers.Location.OriginalString
24                 : null;
25        }
27        //IDisposable stuff...
28    }

And configured as singleton in the DI container.

 2public void ConfigureServices(IServiceCollection services)
 4    services.AddMvc();
 6    //old school version
 7    services.AddSingleton<TrackingHttpClientWrapper>();
 8    //not the best place to put this but... it'll suffice for the sample
 9    ServicePointManager.FindServicePoint(new Uri("")).ConnectionLeaseTimeout = (int)TimeSpan.FromMinutes(1).TotalMilliseconds;
10    ServicePointManager.DnsRefreshTimeout = (int)TimeSpan.FromMinutes(1).TotalMilliseconds;

Also doing ServicePointManager configurations to make sure DNS refreshes as seen here.

Finally the controller action could simply do something like the following.

 2public async Task<IActionResult> TrackAsync(string trackingUrl, CancellationToken ct)
 4    var targetUrl = await _tracker.TrackAsync(trackingUrl, ct);
 5    if (string.IsNullOrWhiteSpace(targetUrl))
 6    {
 7        _logger.LogInformation("No target url -> it wasn't a redirect");
 8    }
 9    else
10    {
11        _logger.LogInformation("Target url: \"{targetUrl}\" -> it was a redirect", targetUrl);
12    }
13    return Ok(new {targetUrl});

Bonus round: same solution ASP.NET Core 2.1 version

With ASP.NET Core 2.1 there is some new stuff to work with HttpClient and avoid all these shenanigans because of port exhaustion and DNS refreshes, so I added to the sample a V2 solution using the new HttpClientFactory features.

 3namespace CodingMilitia.HttpClientNotFollowingRedirectsSample.Web
 5    public class TrackingHttpClientWrapperV2
 6    {
 7        private HttpClient _httpClient;
 9        public TrackingHttpClientWrapperV2(HttpClient httpClient)
10        {
11            _httpClient = httpClient;
12        }
14        public async Task<string> TrackAsync(string trackingUrl, CancellationToken ct)
15        {
16            var response = await _httpClient.GetAsync(trackingUrl, ct);
18            return response.StatusCode == HttpStatusCode.Redirect
19                 ? response.Headers.Location.OriginalString
20                 : null;
21        }
22    }

And to register with the DI container:

 2public void ConfigureServices(IServiceCollection services)
 4    services.AddMvc();
 6    //ASP.NET Core 2.1 version
 7    services.AddScoped<TrackingHttpClientWrapperV2>();
 8    services.AddHttpClient<TrackingHttpClientWrapperV2>()
 9        .ConfigurePrimaryHttpMessageHandler(() =>
10        {
11            return new HttpClientHandler
12            {
13                AllowAutoRedirect = false
14            };
15        });

Other reading material

For more info on ASP.NET Core 2.1 HttpClient related features, Steve Gordon has a series of posts, the first one here.

Wrapping up

Ok, this post is probably bigger than it needed to, just to tell how to configure an http client not to follow redirects, but as I had a real world scenario for its usefulness, I thought I might as well share it.

On a side note, the HttpClientHandler has some other options besides the AllowAutoRedirect I used, so if you’re needing to do something that HttpClient doesn’t appear to do directly, you may want to take a look at what the HttpClientHandler provides.

Thanks for reading!

1 - Fundamental theorem of software engineering

Categories: dotnet
Tags: dotnet aspnet httpclient redirect