FedCM миграция: AbortError, два элемента и 23 промпта с Claude Code

Авторизация через Google One Tap перестала работать в incognito. Ошибка выглядела как баг FedCM, а оказалась - два google-auth элемента на одной странице.

Что произошло

В taskboard-pro авторизация через Google One Tap была критична для конверсии. Пользователь открывает приложение, видит всплывающий промпт со своим гугл-аккаунтом, кликает - и сразу внутри. Без паролей. Это работало - до тех пор, пока Google не начала принудительный перевод трафика на FedCM.

Перевод шёл постепенно: сначала часть пользователей, потом больше. Причина - браузеры убирают поддержку сторонних cookies, и старый Google Sign-In держался именно на них. FedCM заменяет это на уровне платформы: браузер сам медиирует между сайтом и Google, без cookies и редиректов.

Миграция растянулась у меня на несколько часов. Начало было обманчивым - на обычных вкладках всё работало. Потом я открыл incognito и увидел тишину: никакого промпта, никакой ошибки в UI. Только в консоли:

[GSI_LOGGER]: FedCM get() rejects with AbortError: The request has been aborted.

Первая мысль - cooldown период. FedCM ввёл тихий период между попытками автоматической авторизации: если пользователь закрыл промпт крестиком, браузер запоминает и блокирует повторные вызовы на некоторое время. Но в incognito - судя по поведению - никакой истории нет, cooldown не накапливается. Значит, причина в другом.

Что такое FedCM и зачем мигрировать

FedCM - Federated Credential Management - спецификация, находящаяся в статусе First Public Working Draft в W3C с августа 2024 года, но браузеры уже её реализуют. Она позволяет Identity Provider-у (Google, в нашем случае) авторизовывать пользователей без third-party cookies и редиректов. Браузер становится посредником: он знает, что вы залогинены в Google, и передаёт этот факт приложению через нативный UI.

Для разработчика это выглядит просто: добавляешь флаг use_fedcm_for_prompt: true при инициализации Google Identity Services - и всё должно работать. Я так и думал, пока не столкнулся с AbortError.

На практике FedCM оказался строже старого Google Sign-In. Старый GSI прощал многое - несколько вызовов prompt(), дублирующие элементы инициализации. С FedCM я обнаружил другое: concurrent-запросы давали AbortError, два элемента инициализации на странице - тоже. Похоже, браузер допускает только один активный credential request в момент времени, хотя в документации я явного подтверждения этому не нашёл. Дедлайн полной миграции был в августе 2025 - кто не успел к тому моменту, начал терять авторизацию в incognito почти везде.

Баг: AbortError и два элемента

Первые несколько попыток отладки были классическими: проверить сеть, проверить конфигурацию в Google Cloud Console, убедиться что Client ID правильный. Приличное время я провёл в DevTools, глядя на одни и те же запросы. Всё выглядело нормально.

Примерно на 8-м промпте с Claude Code я спросил напрямую: “объясни все возможные причины AbortError при FedCM в incognito”. Claude Code перечислил сценарии: cooldown, concurrent requests, duplicate elements. И сразу пометил: в incognito cooldown маловероятен - значит, смотреть на дубликаты.

Это был поворотный момент. На 9-м промпте я попросил: “найди в проекте все места, где используется google.accounts.id”. Я бы листал файлы по одному, держа контекст в голове. Claude Code прошёлся по всему codebase и выдал список сразу.

Два элемента. В Layout.jsx был <div id="g_id_onload"> с атрибутом data-use_fedcm_for_prompt="true" - добавили при миграции. И в Login.jsx был ещё один такой же <div id="g_id_onload"> - старый, оставшийся с предыдущей реализации. Плюс в Login.jsx - явный вызов google.accounts.id.initialize() с последующим google.accounts.id.prompt().

Три попытки создать FedCM credential request при каждой загрузке страницы авторизации. Убрал дубликат из Login.jsx - ошибка ушла. Старый GSI молча проглатывал такое - показывал один промпт. Именно поэтому баг появился только после миграции, хотя дублирующие элементы существовали раньше.

Промпты с 10-го по 15-й ушли на архитектуру исправления. С 16-го по 23-й - на проверку: “нет ли других мест в коде, где google.accounts.id.prompt вызывается напрямую”. Оказалось, был ещё один вызов в компоненте с кнопкой Sign In. Не виновник изначального бага, но потенциальный источник конфликта в будущем.

Решение - единая точка инициализации:

// Было: инициализация в нескольких местах
// Login.jsx
useEffect(() => {
  google.accounts.id.initialize({
    client_id: CLIENT_ID,
    callback: handleCredentialResponse,
    use_fedcm_for_prompt: true
  });
  google.accounts.id.prompt();
}, []);
// Стало: один модуль, один флаг
// auth.js
let googleInitialized = false;

export function initGoogleIdentity(callback) {
  if (googleInitialized) return;

  google.accounts.id.initialize({
    client_id: CLIENT_ID,
    callback,
    use_fedcm_for_prompt: true,
    auto_select: false
  });

  googleInitialized = true;
}
// App.jsx - единственное место вызова
useEffect(() => {
  initGoogleIdentity(handleCredentialResponse);
  google.accounts.id.prompt();
}, []);

HTML-элемент g_id_onload остался только в корневом Layout, дублирующий JS-вызов убран.

Работает. В incognito тоже.

Как Claude Code помогал отлаживать

23 промпта от первой ошибки до финального фикса - уложился за одну сессию. Большая часть была не про написание кода, а про понимание: почему именно этот сценарий, что ещё могло быть виновато, где ещё смотреть.

Я мог бы часами смотреть в Login.jsx и не найти проблему - потому что проблема была не там. Она была в том, как Login.jsx взаимодействовал с Layout.jsx через глобальный скрипт Google. Claude Code увидел оба файла сразу и объяснил конфликт. Мне было бы легко зациклиться на одном компоненте и пропустить связи между ними.

Главное преимущество AI-ассистента при отладке - он держит весь codebase в голове одновременно. Не отдельный компонент, а зависимости между компонентами. При классическом дебаге ты смотришь в один файл, переключаешься на другой, держишь контекст сам - и когда файлов много, связи теряются.

Выводы

FedCM строже старого Google Sign-In - это не баг, это фича. Браузер стал активным участником авторизации, и он не прощает архитектурных ошибок, которые раньше проглатывались молча.

Правило первое при FedCM: одна инициализация Google Identity на всё приложение. Никаких google.accounts.id.initialize() в компонентах, которые монтируются и анмонтируются. Один модуль, один флаг, один вызов при старте.

Правило второе: при миграции ищи дубликаты. Все g_id_onload в HTML, все вызовы initialize в JS. FedCM найдёт то, что старый GSI игнорировал.

Теперь при любой крупной миграции первым делом прошу Claude Code найти все точки инициализации нового API по всему проекту. Не потому что лень - а потому что cross-file поиск с объяснением зависимостей это именно то, что AI-ассистент делает быстрее при ручном дебаге.

AbortError оказался не ошибкой FedCM. Он был симптомом архитектуры, которая работала по случайности.