Система проверки ботов Cloudflare может незаметно блокировать обычные отправки HTML-форм, превращая простой клик по кнопке оплаты в тупик для реальных пользователей. Переход от навигационного POST-запроса к потоку с использованием fetch (fetch-first flow) восстанавливает нормальную работу, не compromising безопасность.

Почему это важно

Разработчик выпустил форму оплаты, которая безупречно работала во всех тестовых наборах, через curl и на локальном сервере. Однако та же самая форма при использовании Chrome клиентом выдавала ошибку безопасности после первого клика и сообщение «timeout-or-duplicate» после второго. Эта неудача привела к трем срочным выпускам исправлений (hot-fixes) и целому дню отладки.

Скрытая проблема на edge

Форма является частью open-source пакета Astro, который полагается на обычный HTML-элемент <form>. Когда пользователь нажимает Pay, сервер отвечает редиректом 303 на Stripe, и браузер следует за редиректом без использования JavaScript. Сайты используют этот паттерн как резервный вариант на случай, если скрипты отключены.

Cloudflare стоит перед сайтом и запускает движок обнаружения ботов. Для обычных GET-запросов он может показать промежуточную проверку (interstitial challenge) — CAPTCHA или проверку JavaScript. После того как браузер проходит проверку, запрос выполняется.

Однако навигационный POST-запрос нельзя приостановить для прохождения проверки, а затем возобновить с сохранением его тела. Edge-сервер просто отбрасывает запрос и возвращает статус 503, оставляя браузер с пустой страницей или общей ошибкой. Автоматизированные браузеры для тестирования, которые имеют тот же цифровой отпечаток (fingerprint), которому Cloudflare доверяет, никогда не вызывают проверку, поэтому проблема остается невидимой до тех пор, пока на сайт не зайдет реальный пользователь.

Что показали логи

Живой сетевой трассировщик из сессии пользователя в Chrome показал два контрастных запроса к одному и тому же эндпоинту:

  • Navigation POST → ответ 503, вкладка зависла.
  • fetch() POST → запрос выполнен успешно.

Оба запроса исходили из одного источника (origin), содержали одни и те же учетные данные (credentials) и произошли в один и тот же момент. Единственным различием был метод передачи. Запрос fetch обошел промежуточный процесс, который блокирует навигационные POST-запросы.

Пути, которые никуда не вели

Разработчик перепробовал ряд исправлений, которые не затрагивали первопричину:

  • Обновлял токены Turnstile, полагая, что они истекли.
  • Добавлял IP-диапазоны в белый список, думая, что блокировка основана на местоположении.
  • Отключал расширения, очищал service workers и удалял куки.

Каждое изменение не приносило результата, так как сбой происходил выше по стеку, на уровне edge, а не в коде клиента или сервера.

Прагматичное решение

Вместо того чтобы отключать защиту Cloudflare, логику формы переработали, внедрив паттерн fetch-first:

  1. Сбор данных формы и их отправка с помощью fetch() в виде JSON-полезной нагрузки.
  2. Обработка ответа сервера. Если сервер возвращает URL платежного шлюза, вызывается location.assign(), чтобы перейти по нему с помощью простого GET-запроса.

Запросы fetch не вызывают промежуточную проверку, поэтому POST-запрос достигает исходного сервера. Последующий GET-редирект может безопасно пройти через любую проверку, так как тела GET-запросов пусты и их можно повторно отправить после того, как пользователь пройдет проверку.

Риски для разработчиков

  • Доверие пользователей: форма оплаты, которая незаметно дает сбой, подрывает доверие и может привести к потере выручки.
  • Затраты на поддержку: инцидент потребовал трех патчей и целого дня расследований.
  • Слепые зоны тестирования: опора исключительно на внутренние тестовые среды может привести к пропуску пограничных случаев, которые проявляются только в реальных условиях.

Уроки для сообщества

  • Используйте реальные браузеры для сбора данных. Если проблема проявляется только у реальных пользователей, фиксируйте сетевые логи этих сессий, а не полагайтесь только на автоматизированные тесты.
  • Относитесь к edge как к части стека. Cloudflare находится между клиентом и сервером; его поведение влияет на то, как должны быть структурированы запросы.
  • Выбирайте правильный метод передачи. Навигационные POST-запросы и POST-запросы через fetch проходят через разные пути на edge-уровне. Проектируйте API с учетом этого различия.
  • Выводите детали ошибок. Отображайте сообщения 503 или «timeout-or-duplicate» в интерфейсе, чтобы разработчики могли видеть точный характер сбоя, не копаясь в логах.

На что обратить внимание в будущем

Разработчикам следует провести аудит любых рабочих процессов на основе форм, которые полагаются на нативную навигацию через POST, особенно если перед сайтом стоит Cloudflare или аналогичные сервисы безопасности CDN. Добавление легковесной обертки для fetch может предотвратить подобные сбои. Инструменты мониторинга, фиксирующие статус-коды, генерируемые на edge, позволят выявить проблему до того, как она затронет клиентов.

Вывод: Когда активны проверки Cloudflare на наличие ботов, обычная отправка HTML-формы уязвима к скрытым сбоям. Перенаправление POST-запроса через fetch() и завершение процесса с помощью GET-редиректа позволяет обойти ограничение edge-узла, сохраняя при этом безопасность. Относитесь к edge как к коду, а не просто как к промежуточному сетевому узлу, и проектируйте свои транспортные механизмы соответствующим образом.