Система перевірки на ботів Cloudflare може непомітно переривати звичайні відправки HTML-форм, перетворюючи простий клік для оплати на глухий кут для реальних користувачів. Перехід від нативного POST-запиту навігації до потоку із первинним використанням fetch відновлює коректну роботу, не знижуючи рівня безпеки.

Чому це важливо

Розробник випустив форму оплати, яка працювала в кожному тестовому наборі, через curl та на локальному сервері. Та сама форма, коли клієнт використовував Chrome, видавала помилку безпеки після першого кліку та повідомлення «timeout-or-duplicate» після другого. Ця помилка призвела до трьох термінових випусків виправлень (hot-fix) та цілого дня налагодження.

Прихована особливість edge

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

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

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

Що показали логи

Живий слід мережевої активності (network trace) із сесії користувача в Chrome показав два контрастні запити до одного й того самого ендпоінту:

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

Обидва запити походили з одного джерела (origin), мали однакові облікові дані та відбулися в один і той самий момент. Єдина різниця полягала в методі транспортування. Запит fetch оминув проміжний етап, який блокує POST-запити навігації.

Шляхи, що ні до чого не призвели

Розробник спробував серію виправлень, які не зачіпали першопричину:

  • Оновлював токени Turnstile, припускаючи, що вони протерміновані.
  • Додавав IP-діапазони до білих списків, вважаючи, що блокування базується на локації.
  • Вимикав розширення, очищував service workers та видаляв куки.

Кожна зміна не змінювала результат, оскільки помилка виникала вище за потоком на рівні edge, а не в коді клієнта чи сервера.

Прагматичне рішення

Замість того, щоб вимикати захист Cloudflare, форму було перепроектовано з використанням патерну fetch-first:

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

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

Ризики для розробників

  • Довіра користувачів: платіжна форма, яка тихо не спрацьовує, підриває довіру та може призвести до втрати прибутку.
  • Витрати на підтримку: інцидент вимагав трьох патчів та цілого дня розслідування.
  • Сліпі зони тестування: покладання лише на внутрішні тестові середовища може призвести до пропуску помилок у граничних випадках, які з'являються лише в реальних умовах.

Уроки для спільноти

  • Використовуйте реальні браузери для збору даних. Коли проблема виникає лише у реальних користувачів, збирайте мережеві логи цих сесій, а не покладайтеся лише на автоматизовані тести.
  • Розглядайте edge як частину стека. Cloudflare стоїть між клієнтом і сервером; його поведінка впливає на те, як мають бути структуровані запити.
  • Обирайте правильний метод транспортування. Navigation POST та fetch POST проходять через різні шляхи на рівні edge. Проектуйте API з урахуванням цієї різниці.
  • Виводьте деталі помилок. Показуйте повідомлення про статус 503 або «timeout-or-duplicate» в інтерфейсі (UI), щоб розробники могли бачити точний тип помилки, не занурюючись у логи.

На що звернути увагу далі

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

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