#c_sharp #net
Задумал я исправить косяки родного HttpClient. Разумно предположить, что все методы сходятся к SendAsync, а значит нужно переопределять его в наследнике и даже виртуальный он. Но оказалось, что виртуальная только одна его перегрузка, а внутренние методы HttpClient ходят через другие перегрузки и потому в наследник не попадают. Это косяк разработчика класса HttpClient или я чего-то не понимаю в наследовании? Зачем объявлять одну перегрузку virtual, если перегружать ее бесполезно?
Ответы
Ответ 1
Все дело в том, что метод 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 TaskSendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { var requestTask = base.SendAsync(request, cancellationToken); 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 SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { for (var i = 0; i < 3; i++) try { var result = await base.SendAsync(request, cancellationToken).ConfigureAwait(true); return result; } catch (HttpRequestException){} 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
Комментариев нет:
Отправить комментарий