仅针对一个请求在现有 HttpClient 中将 AllowAutoRedirect 设置为 false

Set AllowAutoRedirect false in existing HttpClient for just one request

This answer 关于如何使 HttpClient 不遵循重定向的问题给出了在创建实际客户端时设置的解决方案:

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

答案下方的评论是我的实际问题:

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)?

我现在想要单独的客户端也有一个特定的原因:我希望客户端保留来自早期请求的 cookie。我试图首先执行一些包含有效重定向的请求,但我不想成为链中的 最后一个 重定向。

我搜索过,查看了 .GetAsync(url, ...) 的重载,查看了 HttpClient 的属性和方法,但还没有找到解决方案。

这可能吗?

是的,您可以为每个请求设置 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

您可能已经发现,在发出请求后,您将无法更改 HttpClientHandler 配置。

因为您想要这样做的动机是在请求之间维护 cookie,所以我建议更像这样的东西(不包括 exception/null 参考处理):

    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://whosebug.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();
    }

问题询问是否可以在 个案 的基础上完成以下重定向。虽然对于许多常见情况肯定有用,但我发现现有的答案在这方面有所欠缺。

以下实现允许通过谓词根据具体情况决定是否遵循重定向。 解决方法是重写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://whosebug.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 == "whosebug.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://whosebug.com (which is a URI that redirects to https://whosebug.com):

的三个示例请求
  1. 第一个 GET 请求将遵循重定向,因此我们看到重定向请求响应的状态代码 OK,因为处理程序配置为遵循所有重定向。
  2. 第二个 GET 请求不会遵循重定向,因此我们看到状态代码 Moved,因为处理程序配置为完全遵循重定向到主机 example.com .
  3. 第三个 GET 请求将跟随重定向,因此我们看到重定向请求响应的状态代码 OK,因为处理程序配置为跟随重定向到主机whosebug.com独家。

当然,您可以用任何自定义逻辑替换谓词。