Про баги №1: «фантомное кэширование»

Я решил начать новую, нерегулярную серию коротких заметок про баги, с которыми я сталкиваюсь, и которые мне кажутся интересными. Может из этого сформируется какая-то Всеобъемлющая Теория Багов, возможно нет. Буду рад обратной связи: интересно ли, полезно ли, забавно ли.


Бэкграунд

Я работаю над одним из внутренних приложений в компании. Приложение сделано как SPA на React, которое ходит в бэкенд-сервис по HTTPS.

В приложении есть страница «Рабочее место», где показывается важная статистика для пользователя: количество [redacted], которые требуют внимания. У каждого пользователя есть своя роль: либо он обычный пользователь, либо «супер» (это менеджеры обычных пользователей).

На этой странице, сразу после загрузки приложения в целом, происходит запрос за данными. Если у пользователя обычная роль, то надо добавить в этот запрос фильтр с его почтой, чтобы получить статистику для этого пользователя, а не для всех сразу.

Проблема

Однажды QA тестировали новую фичу и заметили, что в «Рабочем месте» показываются слишком большие числа статистики (для примера, скажем, 100 000 вместо 2000): они больше похожи на числа для всех пользователей, но сверху нарисован фильтр по пользователю.

Проблема #1: пользователь видит данные так, как будто фильтр не применился, но в UI фильтры указаны.

Проблема #2: проблема #1 всплыла довольно поздно, спустя несколько месяцев после релиза, и этой страницей много кто пользуется, почему баг не заметили ранее?

Расследование

Первая реакция — может, эээ, закэшировалось что-то где-то? Но в системе вроде как нет кэширования.

При старте джаваскрипта приложения у нас нет эмейла пользователя — за ним нужно сходить отдельно в бэкенд.

Бизнес-требование про фильтр сделано так: когда приходят данные про почту, мы добавляем фильтр и перезапрашиваем данные.

Но при этом не отменяем первый запрос.

Это я понял, когда, посмотрев в DevTools, увидел что есть два успешных запроса. Но на странице рисуются данные первого запроса, так, словно данные второго запроса были выкинуты.

Но что если второй запрос отрабатывает быстрее первого? Тогда его .then отработает раньше, а уже потом отработает .then первого запроса — и данные перезатрутся.

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

Решение

Отменяем первый запрос, если хотим сделать ещё один. Для этого воспользовался AbortController (и перед перезаписыванием данных чекаем, что аборт-контроллер запроса не сработал — на случай если сетевой запрос отменять уже поздновато).

Выводы

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

Стоит подозревать код в духе:

fetch(req).then(res=> applyResult(res))

Что произойдёт если мы вызовем его дважды?


…если бы мне давали доллар каждый раз, когда меня кусал такой баг на этом проекте, то у меня было бы два доллара. Немного, но всё равно странно, что это случилось дважды.