Задумал я исправить косяки родного HttpClient. Разумно предположить, что все методы сходятся к SendAsync, а значит нужно переопределять его в наследнике и даже виртуальный он.
Но оказалось, что виртуальная только одна его перегрузка, а внутренние методы HttpClient ходят через другие перегрузки и потому в наследник не попадают.
Это косяк разработчика класса HttpClient или я чего-то не понимаю в наследовании? Зачем объявлять одну перегрузку virtual, если перегружать ее бесполезно?
Ответ
Все дело в том, что метод SendAsync не предназначен для расширения. И он не виртуальный, а override метод базового класса HttpMessageInvoker.
Расширять же HttpClient задумано через HttpMessageHandler/DelegatingHandler из которых (собственные реализации через наследование) можно составить цепочку декораторов последовательно обрабатывающих запрос.
Пример:
Проблема 1: HttpClient при таймауте кидает OperationCanceledException, что сбивает с толку и вызывает необходимость вручную проверять "действительно ли была отмена"
Пишем FixTimeoutHandler для подмены исключений. Увы, в SendAsync прокидывается чужой CancellationToken с чужим CancellationTokenSource (есть предположение, что реализация таймаута на нем построена), поэтому в конструктор прокидываем родной CancellationToken и у него проверяем "была ли реальная отмена"
public class FixTimeoutHandler : DelegatingHandler
{
private CancellationToken _cancellationToken;
public FixTimeoutHandler(
HttpClientHandler innerHandler,
CancellationToken cancellationToken) : base(innerHandler)
{
_cancellationToken = cancellationToken;
}
protected override Task
var endTask = requestTask.ContinueWith(
t =>
{
if (t.IsCanceled)
{
//проверяем родной токен, а не тот, что нам дает метод
if (_cancellationToken.IsCancellationRequested)
throw new OperationCanceledException(cancellationToken);
throw new HttpRequestException("Timeout");
}
if (t.IsFaulted)
{
var ex = t.Exception;
ex.Data["RequestUrl"] = request.RequestUri.ToString();
ex.Data["RequestMethod"] = request.Method.Method;
throw ex;
}
return t.Result;
}, TaskContinuationOptions.ExecuteSynchronously
);
return endTask;
}
}
Проблема 2: Повторять запрос при сетевых ошибках (в том числе и по таймауту, который мы хендлером FixTimeoutHandler определили в сетевые ошибки. Любая другая ошибка пролетит выше по стеку.
Упрощенная реализация
public class RepeatHandler : DelegatingHandler
{
public RepeatHandler(HttpMessageHandler innerHandler) : base(innerHandler)
{
}
protected override async Task
throw new HttpRequestException("Error");
}
}
Последний в цепочке HttpClientHandler, который делает http запросы.
Собираем вместе:
var cts = new CancellationTokenSource();
var http = new HttpClient(new RepeatHandler(new FixTimeoutHandler(new HttpClientHandler(),_cts.Token)))
{
Timeout = TimeSpan.FromSeconds(3)
};
var result=await http.GetAsync("http://localhost/sleep.php", _cts.Token);
Также есть класс-фабрика HttpClientFactory для сбора цепочек хендлеров, но он лежит в отдельной сборке, которую нужно ставить через nuget
Комментариев нет:
Отправить комментарий