为现有的HttpClient在单个请求中设置AllowAutoRedirect为false

3

这个回答针对如何让HttpClient不遵循重定向的问题,提供了在创建实际客户端时设置的解决方案:

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

下面的评论是我实际的问题:
“在不需要两个独立的HttpClient实例(允许重定向和不允许重定向)的情况下,是否可以基于每个请求来完成这个操作?” 我有一个特定的原因,不想要单独的客户端:我希望客户端保留之前请求中的cookie。我正在尝试执行一些包括有效重定向的请求,但只有链中的最后一个请求不应该是重定向。 我搜索了.GetAsync(url,...)的重载,并查看了HttpClient的属性和方法,但尚未找到解决方案。这可能吗?
4个回答

3
这个问题询问是否可以根据具体情况进行重定向。虽然对于许多常见情况非常有用,但我发现现有的答案在这方面缺乏。下面的实现允许通过谓词在真正的逐案基础上配置是否跟随重定向的决策。解决方法是覆盖HttpClientHandler的SendAsync()方法。
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;
        }
    }
}

Main方法展示了三个例子请求到http://stackoverflow.com(这是一个重定向到https://stackoverflow.com的URI):
  1. 第一个GET请求将跟随重定向,因此我们看到重定向请求的响应状态码OK,因为处理程序配置为跟随所有重定向。
  2. 第二个GET请求将不会跟随重定向,因此我们看到状态码Moved,因为处理程序配置为仅跟随到example.com主机的重定向。
  3. 第三个GET请求将跟随重定向,因此我们看到重定向请求的响应状态码OK,因为处理程序配置为仅跟随到stackoverflow.com主机的重定向。
当然,您可以为谓词替换任何自定义逻辑。

你的示例代码为每个请求创建了一个新的 HttpClient 实例,然后将其处理掉。尽管 HttpClient 实现了 IDisposable 接口,但这并不是推荐使用 HttpClient 类的方式。请参阅 https://www.aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/ 和 https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient-guidelines。人们不应该直接使用这段代码。 - Kevin

2

你可能已经发现,在发送请求后,不允许更改HttpClientHandler的配置。

因为你想要保持请求之间的cookie,所以我建议采用以下方法(未包含异常/空引用处理):

    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;
    }

测试:

    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();
    }

1
如果您在初始化“HttpClientHandler”时不使用“对象初始化程序”,则可以更改“HttpClientHandler”的配置。请参阅我的示例。谢谢。 - Kenan Güler
1
我刚在Ubuntu上运行了它。让我在Win 10上检查一下。 - Kenan Güler
1
所以我刚刚在Win 10上运行了相同的测试(使用dotnet版本“3.1.102”),并且得到了相同的积极结果-同时使用“XUnit”和“NUnit”框架。 - Kenan Güler
1
@KenanGüler,我相信你...我在实际应用中运行了它。单元测试可能每次都会重新初始化,这是唯一能解释这种行为的事情。 - rfmodulator
1
好的,谢谢你的提示。嗯,在这种情况下,你的建议将是唯一的解决方案。然而,在测试环境中,我的代码完美地运行 - 并且也使用了 CookieContainer。 :D - Kenan Güler
显示剩余7条评论

2

是的,您可以针对每个请求设置HttpClientHandler的属性,例如:

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

确保只有一个线程同时使用HttpClient如果客户端处理程序设置不同。


示例(注意:仅在测试环境中有效)

在本地主机上运行的虚拟 Node.js 远程服务器:

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}!`))

测试

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);
        }
    }
}

通过 dotnet test 输出:

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

0

三年后,这是我的实现:

//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;
        }
    }
}

主要思路是禁用自动重定向并使用自定义RedirectHandler手动处理它们。

  1. 在发送请求之前,我们使用扩展方法SetAutoRedirect将重定向规则存储在请求的选项字典中。
  2. 收到响应后,我们检查它是否是重定向。如果是,则使用扩展方法GetAutoRedirect检查请求的选项字典中是否有重定向规则。
  3. 重复步骤#2,直到达到MaxAutomaticRedirections或没有进一步的重定向为止。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接