Angular

Доступ к данным из бэкенда — основа почти каждого одностраничного приложения. Весь динамический контент загружается с сервера.

В большинстве случаев HTTP-запросы работают хорошо и возвращают желаемый результат. Но, бывает, запрос выполняется неверно.

Skillbox

Представьте: кто-то использует ваш веб-сайт через точку доступа в поезде, едущем со скоростью 200 км/ч. 🚅 Да, скорость сети может быть низкой, но HTTP-запрос все еще возвращает желаемый результат.

Но что если поезд въедет в тоннель? Высоки шансы, что пользователь потеряет связь, и веб-приложение не сможет достучаться до сервера. В этом случае пользователь вынужден обновить приложение, как только выедет из тоннеля и вернется в сеть.

Обновление влияет на текущее состояние приложения. Пользователь может, например, потерять данные, которые ввел в форму.

Вместо того, чтобы позволить запросу мгновенно неудачно завершиться, лучше повторить его несколько раз и отобразить сообщение пользователю. В этом случае обновление можно предотвратить.

Повторение неудачных запросов

Давайте сэмулируем ситуацию с поездом на примере бэкенда, который завершается после трех попыток и возвращает данные на четвертой.

В Angular мы создаем сервис и внедряем HttpClient, чтобы получить данные из бэкенда.

Здесь ничего особенного. Мы внедряем HTTPClient и выполняем простой запрос get . Если этот запрос вернет ошибку, мы обработаем ее и вернем пустой Observable, чтобы продолжить выполнение.

Так большинство приложений выполняет HTTP-запрос. В примере выше запрос выполняется один раз, затем либо возвращаются данные из бэкенда, либо запрос завершается неудачно.

Но как нам повторить запрос, если конечная точка приветствия не доступна или возвращает ошибку? Может, у RxJS есть оператор? Конечно, есть. У RxJS есть операторы на все случаи жизни. 😉

Первая вещь, которая приходит в голову — оператор retry. Давайте посмотрим на его определение.

Возвращает Observable, отражающий исходный Observable c исключением, возникшим из-за ошибки. Если исходный Observable вызывает ошибку, этот метод будет повторно подписываться на исходный Observable максимальное количество раз count(заданное как числовой параметр), а не распространять вызов error.

Звучит как нужный нам оператор. Давайте включим его в нашу цепочку.

Мы успешно применили оператор retry. Давайте проверим, как он влияет на поведение HTTP-запроса внутри приложения-примера.

Приложение супер простое — оно просто выполняет HTTP-вызов, когда мы нажимаем на кнопку «вызвать сервер».

Как упоминалось ранее, бэкенд возвращает ошибку в первые три попытки и доставляет ответ на четвертую — мы можем увидеть работу оператора повтора на вкладке Network в панели разработчика.

Прекрасно! Повторили.🤘

Но есть что улучшить. Обратите внимание, что повторы выполняются немедленно, что не особо пригодится в нашем примере с тоннелем.🤔

Отложенные повторные попытки

Мы не выезжаем из тоннеля сразу же после въезда в него — мы проводим там некоторое время. Следовательно, нам нужно растянуть период повтора, откладывая каждую попытку.

Нам нужен более тонкий контроль над поведением повторов, чтобы решать, когда повторы должны выполняться. Тут оператора retry недостаточно. Давайте снова обратимся к документам RxJs.

Кажется, операторretryWhen подойдет. Вот определение из официальных документов:

Возвращает Observable, отражающий исходный Observable c исключением, возникшим из-за ошибки. Если Observable вызывает ошибку, этот метод вызывает Throwable, который провоцирует ошибку Observable, возвращенного из notifier . Если Observable вызывает complete или error, тогда метод вызывает complete илиerrorдочерней подписки. В противном случае, этот метод повторно подпишется на исходный Observable.

Что? 😶Звучит очень сложно. Попробую объяснить проще.

Оператор retryWhenпринимает обратный вызов, который возвращает Observable. Возвращенный Observable определяет поведение оператораretryWhen, основываясь на следующих правилах:

Оператор retryWhen:

— повторяет исходный Observable, если возвращенный Observable успешно эмитится;

— завершается и выдает ошибку, когда возвращенный Observable выдает ошибку;

— завершается, если возвращенный Observable завершается.

Сам обратный вызов вызывается только тогда, когда исходный Observable завершается неудачно в первый раз.


Используя эти знания, напишем отложенную повторную попытку с помощью оператора retryWhen.

Если исходный Observable, наш HTTP-запрос, выполняется неудачно, вызывается оператор retryWhen. Внутри обратного вызова мы получаем доступ к ошибке, которая спровоцировала сбой. Мы задерживаем errors, уменьшаем количество попыток и возвращаем новый Observable, который эмитит ошибку.

Основываясь на правилах оператора retryWhen, этот Observable запускает актуальный повтор, потому что он эмитит значение. Если попытки завершаются неудачно несколько раз и значение переменной retriesпадает до 0, мы сдаемся и отбрасываем ошибку.

🤠Круто! Возьмем фрагмент выше и заменим его оператором повтора в цепочке. Но погодите. 

Как насчет переменной попыток? Эта переменная содержит текущее состояние повторов. Где задана эта переменная? Когда состояние сбрасывается? Состояние должно управляться изнутри потока, а не снаружи.

Создаем пользовательский оператор delayedRetry

Извлекая код в отдельный оператор RxJS, мы решим проблему состояния и улучшим читабельность кода.

Существуют различные способы выполнения оператора RxJS. Выбор метода сильно зависит от того, как построен оператор.

Наш оператор построен на основе существующих RxJS операторов, поэтому мы пойдем по простому пути. В нашей задаче RxJS оператор просто функция со следующей сигнатурой:

const customOperator = (src: Observable<A>) => Observable<B>

Оператор принимает исходный Observable и возвращает другой.

Поскольку пользовательский оператор позволяет пользователю решать, с каким интервалом и как часто повторять попытки, нужно завернуть определение функции, указанной выше, в фабричную функцию, которая принимает delayMs иmaxRetry в качестве параметров.

const customOperator = (delayMs: number, maxRetry: number) => {
return (src: Observable<A>) => Observable<B>
}

⚠️Если вы хотите создать оператор, не состоящий из существующих операторов, стоит обратить внимание на обработку ошибки и подписки. Кроме того, нужно расширить класс Observable и реализовать функциюlift . (Читать подробнее)

Итак, основываясь на фрагменте выше, напишем наш пользовательский оператор Rx.

Отлично. Оператор доступен, его можно импортировать. Давайте используем новый оператор в HTTP-запросе.

Мы поместили оператор delayedRetryв цепочку и ввели параметры 1000 и 3. Первый параметр определяет в миллисекундах задержку между повторными попытками. Второй параметр определяет максимальное число повторных попыток.

Перезапустим приложение и посмотрим на оператор в действии.

Мы видим, что каждая повторная попытка задерживается и корректный ответ отображается пользователю сразу при поступлении.

Повторная попытка, вернуться, повторить!

В предыдущей попытке мы задерживали каждый запрос на одинаковое время.

Таким образом увеличивается задержка после каждой попытки. Первая повторная попытка происходит через 1 с, вторая — через 2, третья — через 3.

Давайте создадим новый оператор retryWithBackoff, который внедрит это.

Сейчас при запуске приложения мы видим, как увеличивается задержка.

После каждой повторной попытки мы возвращаемся назад и увеличиваем задержку. Как только источник возвращает корректный ответ, мы отображаем его в пользовательском интерфейсе.

ВЫВОД

Повторные попытки HTTP-запросов позволяют приложению работать стабильнее.

Этот метод особенно полезен для критических запросов, принимающих данные, которые необходимы для работы приложения. Например, конфигурационные данные с URL-адресами бэкендов.

В большинстве сценариев RxJS оператора retry недостаточно, поскольку он немедленно повторяет неудачные запросы. Оператор retryWhen дает больше контроля над повторными попытками, позволяя определять, когда попытка произойдет. С этими операторами можно запускать отложенные попытки и попытки с возвратом.

При реализации многоразового поведения в RxJS цепочке рекомендуется извлечь логику в новый оператор.

Читать также:


Перевод статьи Kevin Kreuzer: Retry failed HTTP requests in Angular