Reputation: 63739
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
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.
Upvotes: 0
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):
Of course, you can substitute any custom logic for the predicate.
Upvotes: 3
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.
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
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