Based on the answers by Darrel Miller (https://stackoverflow.com/a/19493338/227779) and Jürgen Steinblock (https://stackoverflow.com/a/60277550), I've constructed the following handler. My changes:
- Updated to be in sync with modern C# (
async/await)
- Complete example (Jürgen's example only included the
CopyRequest method + some details on how to change the call site)
- List of status codes slightly different - the numeric values are the same, but Darrel's list contained a few duplicates
- Supports redirects in multiple levels (i.e.
302 Found -> 302 Found -> 200 OK), which was needed for our use case. A maximum of 10 levels of redirects is set, to make sure we break if we encounter redirect loops.
HttpClient creation
Use like this:
var innerHandler = new HttpClientHandler {
AllowAutoRedirect = false
};
var withAuthorizationHandler = new SameHostAllowRedirectWithAuthorizationHandler {
InnerHandler = innerHandler
};
var client = new HttpClient(withAuthorizationHandler);
SameHostAllowRedirectWithAuthorizationHandler implementation
// Based on examples from https://stackoverflow.com/a/19493338/227779 (posted by Darrel Miller
// (MIT-licensed)) and https://stackoverflow.com/a/60277550 (posted by Jürgen Steinblock (CC
// BY-SA 4.0))
private class SameHostAllowRedirectWithAuthorizationHandler : DelegatingHandler {
private const int MaxRedirects = 10;
private readonly HttpStatusCode[] RedirectStatusCodes = {
HttpStatusCode.MovedPermanently,
HttpStatusCode.Found,
HttpStatusCode.SeeOther,
// 304 Not Modified is not a redirect
// 305 Use Proxy ignored
// 306 Switch Proxy no longer used
HttpStatusCode.TemporaryRedirect,
HttpStatusCode.PermanentRedirect
};
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
var response = await base.SendAsync(request, cancellationToken);
for (int i = 0; i < MaxRedirects; i++) {
if (RedirectStatusCodes.Contains(response.StatusCode)) {
var newRequest = CopyRequest(response);
response = await base.SendAsync(newRequest, cancellationToken);
}
else {
return response;
}
}
throw new Exception($"Maximum number of redirects ({MaxRedirects}) reached. Is there a redirect loop somewhere?");
}
private static HttpRequestMessage CopyRequest(HttpResponseMessage response) {
var oldRequest = response.RequestMessage!;
var newRequest = new HttpRequestMessage(oldRequest.Method, oldRequest.RequestUri);
if (response.Headers.Location != null) {
if (response.Headers.Location.IsAbsoluteUri) {
newRequest.RequestUri = response.Headers.Location;
}
else {
newRequest.RequestUri = new Uri(newRequest.RequestUri, response.Headers.Location);
}
}
foreach (var header in oldRequest.Headers) {
if (header.Key.Equals("Authorization", StringComparison.OrdinalIgnoreCase) && !(oldRequest.RequestUri.Host.Equals(newRequest.RequestUri.Host))) {
// Do not leak Authorization Header to other hosts
continue;
}
newRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
foreach (var property in oldRequest.Properties) {
newRequest.Properties.Add(property);
}
if (response.StatusCode == HttpStatusCode.Redirect || response.StatusCode == HttpStatusCode.Found || response.StatusCode == HttpStatusCode.SeeOther) {
newRequest.Content = null;
newRequest.Method = HttpMethod.Get;
}
else if (oldRequest.Content != null) {
newRequest.Content = new StreamContent(oldRequest.Content.ReadAsStreamAsync().Result);
}
return newRequest;
}
}