Страницы

Поиск по вопросам

вторник, 12 февраля 2019 г.

Переопределение поведения HttpClient

Задумал я исправить косяки родного 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 SendAsync(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

Комментариев нет:

Отправить комментарий