Jeroen
Jeroen

Reputation: 63739

Set AllowAutoRedirect false in existing HttpClient for just one request

This answer to the question on how to make HttpClient not follow redirects gives a solution to be set upon creating the actual client:

var handler = new HttpClientHandler { AllowAutoRedirect = false };    
var client = new HttpClient(handler);

The comment below the answer is my actual question:

Is it possible to do this on a per-request basis without needing two separate HttpClient instances (i.e. one that allows redirects and one that does not)?

I have a specific reason too for now wanting separate clients: I want the client to retain its cookies from earlier requests. I'm trying to do a few requests first that include valid redirects, but only the last one in the chain I don't want to be a redirect.

I've searched, and looked through the overloads of .GetAsync(url, ...), and looked through the properties and methods of HttpClient, but found no solution yet.

Is this possible?

Upvotes: 3

Views: 3705

Answers (4)

w4po
w4po

Reputation: 41

3 years later, here is my implementation:

//Usage:
var handler = new RedirectHandler(new HttpClientHandler());
var client = new HttpClient(handler);

//redirects to HTTPS
var url = "http://stackoverflow.com/";

//AutoRedirect is true
var response = await HttpClientHelper.SendAsync(client, url, autoRedirect: true).ConfigureAwait(false);
//AutoRedirect is false
response = await HttpClientHelper.SendAsync(client, url, autoRedirect: false).ConfigureAwait(false);

public static class HttpClientHelper
{
    private const string AutoRedirectPropertyKey = "RequestAutoRedirect";
    private static readonly HttpRequestOptionsKey<bool?> AutoRedirectOptionsKey = new(AutoRedirectPropertyKey);

    public static Task<HttpResponseMessage> SendAsync(HttpClient client, string url, bool autoRedirect = true)
    {
        var uri = new Uri(url);
        var request = new HttpRequestMessage
        {
            RequestUri = uri,
            Method = HttpMethod.Get
        };
        
        request.SetAutoRedirect(autoRedirect);

        return client.SendAsync(request);
    }

    public static void SetAutoRedirect(this HttpRequestMessage request, bool autoRedirect)
    {
        request.Options.Set(AutoRedirectOptionsKey, autoRedirect);
    }
    public static bool? GetAutoRedirect(this HttpRequestMessage request)
    {
        request.Options.TryGetValue(AutoRedirectOptionsKey, out var value);
        return value;
    }

    public static HttpMessageHandler? GetMostInnerHandler(this HttpMessageHandler? self)
    {
        while (self is DelegatingHandler handler)
        {
            self = handler.InnerHandler;
        }

        return self;
    }
}

public class RedirectHandler : DelegatingHandler
{
    private int MaxAutomaticRedirections { get; set; }
    private bool InitialAutoRedirect { get; set; }

    public RedirectHandler(HttpMessageHandler innerHandler) : base(innerHandler)
    {
        var mostInnerHandler = innerHandler.GetMostInnerHandler();
        SetupCustomAutoRedirect(mostInnerHandler);
    }

    private void SetupCustomAutoRedirect(HttpMessageHandler? mostInnerHandler)
    {
        //Store the initial auto-redirect & max-auto-redirect values.
        //Disabling auto-redirect and handle redirects manually.
        try
        {
            switch (mostInnerHandler)
            {
                case HttpClientHandler hch:
                    InitialAutoRedirect = hch.AllowAutoRedirect;
                    MaxAutomaticRedirections = hch.MaxAutomaticRedirections;
                    hch.AllowAutoRedirect = false;
                    break;
                case SocketsHttpHandler shh:
                    InitialAutoRedirect = shh.AllowAutoRedirect;
                    MaxAutomaticRedirections = shh.MaxAutomaticRedirections;
                    shh.AllowAutoRedirect = false;
                    break;
                default:
                    Debug.WriteLine("[SetupCustomAutoRedirect] Unknown handler type: {0}", mostInnerHandler?.GetType().FullName);
                    InitialAutoRedirect = true;
                    MaxAutomaticRedirections = 17;
                    break;
            }
        }
        catch (Exception e)
        {
            Debug.WriteLine(e.Message);
            InitialAutoRedirect = true;
            MaxAutomaticRedirections = 17;
        }
    }

    private bool IsRedirectAllowed(HttpRequestMessage request)
    {
        var value = request.GetAutoRedirect();
        if (value == null)
            return InitialAutoRedirect;
        
        return value == true;
    }
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var redirectCount = 0;
        var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);

        //Manual Redirect
        //https://github.com/dotnet/runtime/blob/ccfe21882e4a2206ce49cd5b32d3eb3cab3e530f/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/RedirectHandler.cs
        Uri? redirectUri;
        while (IsRedirect(response) && IsRedirectAllowed(request) && (redirectUri = GetUriForRedirect(request.RequestUri!, response)) != null)
        {
            redirectCount++;
            if (redirectCount > MaxAutomaticRedirections)
                break;

            response.Dispose();

            // Clear the authorization header.
            request.Headers.Authorization = null;
            // Set up for the redirect
            request.RequestUri = redirectUri;

            if (RequestRequiresForceGet(response.StatusCode, request.Method))
            {
                request.Method = HttpMethod.Get;
                request.Content = null;
                if (request.Headers.TransferEncodingChunked == true)
                    request.Headers.TransferEncodingChunked = false;
            }

            // Issue the redirected request.
            response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
        }

        return response;
    }
    private bool IsRedirect(HttpResponseMessage response)
    {
        switch (response.StatusCode)
        {
            case HttpStatusCode.MultipleChoices:
            case HttpStatusCode.Moved:
            case HttpStatusCode.Found:
            case HttpStatusCode.SeeOther:
            case HttpStatusCode.TemporaryRedirect:
            case HttpStatusCode.PermanentRedirect:
                return true;

            default:
                return false;
        }
    }
    private static Uri? GetUriForRedirect(Uri requestUri, HttpResponseMessage response)
    {
        var location = response.Headers.Location;
        if (location == null)
        {
            return null;
        }

        // Ensure the redirect location is an absolute URI.
        if (!location.IsAbsoluteUri)
        {
            location = new Uri(requestUri, location);
        }

        // Per https://tools.ietf.org/html/rfc7231#section-7.1.2, a redirect location without a
        // fragment should inherit the fragment from the original URI.
        var requestFragment = requestUri.Fragment;
        if (!string.IsNullOrEmpty(requestFragment))
        {
            var redirectFragment = location.Fragment;
            if (string.IsNullOrEmpty(redirectFragment))
            {
                location = new UriBuilder(location) { Fragment = requestFragment }.Uri;
            }
        }

        return location;
    }
    private static bool RequestRequiresForceGet(HttpStatusCode statusCode, HttpMethod requestMethod)
    {
        switch (statusCode)
        {
            case HttpStatusCode.Moved:
            case HttpStatusCode.Found:
            case HttpStatusCode.MultipleChoices:
                return requestMethod == HttpMethod.Post;
            case HttpStatusCode.SeeOther:
                return requestMethod != HttpMethod.Get && requestMethod != HttpMethod.Head;
            default:
                return false;
        }
    }
}

The main idea is to disable automatic redirects and handle them manually using a custom RedirectHandler.

  1. Before sending a request, we utilize the extension method SetAutoRedirect to store the redirect rule in the request's options dictionary.
  2. Upon receiving a response, we check if it is a redirect. If it is, we then examine the request's options dictionary for a redirect rule using the extension method GetAutoRedirect.
  3. Repeat #2 until MaxAutomaticRedirections is reached or there are no further redirects.

Upvotes: 0

Hi4s
Hi4s

Reputation: 39

The question asks whether following redirects can be done on a case-by-case basis. While certainly useful for many common cases, I found the existing answers lacking in that regard.

The following implementation allows the decision on whether to follow a redirect or not to be configured on a true case-by-case basis via a predicate. The solution is to override the SendAsync() method of HttpClientHandler.

using System;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace HttpClientCustomRedirectBehavior
{
    static class Program
    {
        private const string REDIRECTING_URL = "http://stackoverflow.com/";

        static async Task Main(string[] args)
        {
            HttpMessageHandler followRedirectAlwaysHandler = new RestrictedRedirectFollowingHttpClientHandler(
                response => true);
            HttpMessageHandler followRedirectOnlyToSpecificHostHandler = new RestrictedRedirectFollowingHttpClientHandler(
                response => response.Headers.Location.Host == "example.com");

            HttpResponseMessage response;
            using (HttpClient followRedirectAlwaysHttpClient = new HttpClient(followRedirectAlwaysHandler))
            {
                response = await followRedirectAlwaysHttpClient.GetAsync(REDIRECTING_URL);
                Console.WriteLine(response.StatusCode); // OK
            }

            using (HttpClient followRedirectOnlyToSpecificHostHttpClient = new HttpClient(followRedirectOnlyToSpecificHostHandler))
            {
                response = await followRedirectOnlyToSpecificHostHttpClient.GetAsync(REDIRECTING_URL);
                Console.WriteLine(response.StatusCode); // Moved
            }

            followRedirectOnlyToSpecificHostHandler = new RestrictedRedirectFollowingHttpClientHandler(
                response => response.Headers.Location.Host == "stackoverflow.com");
            using (HttpClient followRedirectOnlyToSpecificHostHttpClient = new HttpClient(followRedirectOnlyToSpecificHostHandler))
            {
                response = await followRedirectOnlyToSpecificHostHttpClient.GetAsync(REDIRECTING_URL);
                Console.WriteLine(response.StatusCode); // OK
            }
        }
    }

    public class RestrictedRedirectFollowingHttpClientHandler : HttpClientHandler
    {
        private static readonly HttpStatusCode[] redirectStatusCodes = new[] {
                     HttpStatusCode.Moved,
                     HttpStatusCode.Redirect,
                     HttpStatusCode.RedirectMethod,
                     HttpStatusCode.TemporaryRedirect,
                     HttpStatusCode.PermanentRedirect
                 };

        private readonly Predicate<HttpResponseMessage> isRedirectAllowed;

        public override bool SupportsRedirectConfiguration { get; }

        public RestrictedRedirectFollowingHttpClientHandler(Predicate<HttpResponseMessage> isRedirectAllowed)
        {
            AllowAutoRedirect = false;
            SupportsRedirectConfiguration = false;
            this.isRedirectAllowed = response => {
                return Array.BinarySearch(redirectStatusCodes, response.StatusCode) >= 0
              && isRedirectAllowed.Invoke(response);
            };
        }

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            int redirectCount = 0;
            HttpResponseMessage response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
            while (isRedirectAllowed.Invoke(response)
                && (response.Headers.Location != request.RequestUri || response.StatusCode == HttpStatusCode.RedirectMethod && request.Method != HttpMethod.Get)
                && redirectCount < this.MaxAutomaticRedirections)
            {
                if (response.StatusCode == HttpStatusCode.RedirectMethod)
                {
                    request.Method = HttpMethod.Get;
                }
                request.RequestUri = response.Headers.Location;
                response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
                ++redirectCount;
            }
            return response;
        }
    }
}

The Main method shows three example requests to http://stackoverflow.com (which is a URI that redirects to https://stackoverflow.com):

  1. The first GET request will follow the redirect and therefore we see the status code OK of the response to the redirected request, because the handler is configured to follow all redirects.
  2. The second GET request will not follow the redirect and therefore we see the status code Moved, because the handler is configured to follow redirects to the host example.com exclusively.
  3. The third GET request will follow the redirect and therefore we see the status code OK of the response to the redirected request, because the handler is configured to follow redirects to the host stackoverflow.com exclusively.

Of course, you can substitute any custom logic for the predicate.

Upvotes: 3

Kenan G&#252;ler
Kenan G&#252;ler

Reputation: 2003

Yes, you can set the properties of the HttpClientHandler per each request, like so:

using (var handler = new HttpClientHandler())
using (var client = new HttpClient(handler))
{
    handler.AllowAutoRedirect = false;
    // do your job
    handler.AllowAutoRedirect = true;
}

Just make sure that only one thread consumes the HttpClient at a time, if the client handler settings are different.


Example (note: only works in test environment)

Dummy remote server with Node.js runnin on localhost:

const express = require('express')
const app = express()
const cookieParser = require('cookie-parser')
const session = require('express-session')
const port = 3000

app.use(cookieParser());
app.use(session({secret: "super secret"}))

app.get('/set-cookie/:cookieName', (req, res) => {
    const  cookie = Math.random().toString()
    req.session[req.params.cookieName] = cookie
    res.send(cookie)
});

app.get('/ok', (req, res) => res.send('OK!'))

app.get('/redirect-301', (req, res) => {
    res.writeHead(301, {'Location': '/ok'})
    res.end();
})

app.get('/get-cookie/:cookieName', (req, res) => res.send(req.session[req.params.cookieName]))

app.listen(port, () => console.log(`App listening on port ${port}!`))

Tests

using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using NUnit.Framework;

public class Tests
{
    private HttpClientHandler handler;
    private HttpClient client;
    private CookieContainer cookieJar = new CookieContainer();
    private string cookieName = "myCookie";
    private string cookieValue;

    [SetUp]
    public void Setup()
    {
        handler = new HttpClientHandler()
        {
            AllowAutoRedirect = true,
            CookieContainer = cookieJar
        };
        client = new HttpClient(handler);
    }

    [Test]
    public async Task Test0()
    {
        using (var response = await client.GetAsync($"http://localhost:3000/set-cookie/{cookieName}"))
        {
            Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
            cookieValue = await response.Content.ReadAsStringAsync();
        }
    }

    [Test]
    public async Task Test1()
    {
        handler.AllowAutoRedirect = true;
        using (var response = await client.GetAsync("http://localhost:3000/redirect-301"))
        {
            Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
            Assert.AreEqual(await response.Content.ReadAsStringAsync(), "OK!");
        }
    }

    [Test]
    public async Task Test2()
    {
        handler.AllowAutoRedirect = false;
        using (var response = await client.GetAsync("http://localhost:3000/redirect-301"))
        {
            Assert.AreEqual(HttpStatusCode.MovedPermanently, response.StatusCode);
        }
    }

    [Test]
    public async Task Test3()
    {
        using (var response = await client.GetAsync($"http://localhost:3000/get-cookie/{cookieName}"))
        {
            Assert.AreEqual(await response.Content.ReadAsStringAsync(), cookieValue);
        }
    }
}

Output via dotnet test:

Test Run Successful.
Total tests: 4
     Passed: 4
 Total time: 0.9352 Seconds

Upvotes: 2

rfmodulator
rfmodulator

Reputation: 3738

As you've probably discovered, you're not allowed to change the HttpClientHandler configuration after a request has been made.

Because your motivation for wanting to do this is to maintain the cookies between requests, then I propose something more like this (no exception/null reference handling included):

    static CookieContainer cookieJar = new CookieContainer();

    static async Task<HttpResponseMessage> GetAsync(string url, bool autoRedirect)
    {
        HttpResponseMessage result = null;

        using (var handler = new HttpClientHandler())
        using (var client = new HttpClient(handler))
        {
            handler.AllowAutoRedirect = autoRedirect;
            handler.CookieContainer = cookieJar;

            result = await client.GetAsync(url);

            cookieJar = handler.CookieContainer;
        }

        return result;
    }

Test:

    static async Task Main(string[] args)
    {
        string url = @"http://stackoverflow.com";

        using (var response = await GetAsync(url, autoRedirect: false))
        {
            Console.WriteLine($"HTTP {(int)response.StatusCode} {response.StatusCode}");
            Console.WriteLine($"{response.Headers}");

            Console.WriteLine("Cookies:");
            Console.WriteLine($"{cookieJar.GetCookieHeader(new Uri(url))}\r\n");
        }

        Console.WriteLine(new string('-', 30));

        using (var response = await GetAsync(url, autoRedirect: true))
        {
            Console.WriteLine($"HTTP {(int)response.StatusCode} {response.StatusCode}");
            Console.WriteLine($"{response.Headers}");

            Console.WriteLine("Cookies:");
            Console.WriteLine($"{cookieJar.GetCookieHeader(new Uri(url))}\r\n");
        }

        Console.ReadLine();
    }

Upvotes: 2

Related Questions