Как бот ушёл в 7273 год: бесконечный календарь, 88 632 запроса и неожиданный нагрузочный тест Laravel

от автора

Недавно я запустил небольшой хобби-проект GameStory — сервис, в котором можно собирать игровые коллекции, отмечать прохождения, выставлять оценки и сохранять свою игровую историю. Изначально проект планировался вообще как личный, но, увидев поддержку друзей, я решил превратить его во что-то большее.

Одной из функций проекта стал календарь прохождений. Он показывает, в какие дни пользователь играл, начинал или заканчивал игры. Я рассчитывал, что календарём будут пользоваться люди — ну или хотя бы один человек: я.

Первым по-настоящему заинтересовался робот.

За один день он открыл мой календарь 88 632 раза, дошёл до 7273 года и заодно бесплатно провёл нагрузочное тестирование проекта.

Немного о проекте

GameStory написан на Laravel.

Пользователь может:

  • создавать несколько списков игр;

  • импортировать библиотеку из Steam;

  • добавлять игры из обычного текстового списка;

  • отмечать статусы и периоды прохождения;

  • ставить оценки и писать впечатления;

  • смотреть историю игр в виде календаря;

  • пользоваться другими функциями: друзьями, достижениями, профилями и т. д.

Календарь не был одной из моих любимых функций, но он превращал обычный список пройденных игр в небольшую личную временную линию.

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

/calendar/chrono

Между месяцами можно было перемещаться стрелками вперёд и назад. Технически выбранный месяц передавался через GET-параметр:

/calendar/chrono?month=2026-08/calendar/chrono?month=2026-09/calendar/chrono?month=2026-10

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

Собственная система посещений

Кроме Google Analytics и Яндекс Метрики, я сделал простую собственную систему учёта визитов.

Она сохраняла:

  • IP-адрес;

  • User-Agent;

  • тип устройства;

  • браузер;

  • посещённый путь;

  • время визита.

Мне было интереснее видеть не только количество пользователей, но и их реальные маршруты:

Главная → профиль → список игр → календарь → авторизация

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

Сначала я не придал этому большого значения. Затем количество записей в таблице визитов превысило 128 тысяч. Из них 88 632 запроса за один день приходились на:

/calendar/chrono

Почему все запросы выглядели одинаково

В статистике я сохранял путь примерно так:

$request->path()

GET-параметры при этом не попадали в запись. Поэтому следующие адреса выглядели одинаково:

/calendar/chrono?month=2026-08/calendar/chrono?month=2047-03/calendar/chrono?month=7273-12

В базе сохранялось только:

calendar/chrono

Я видел почти девяносто тысяч обращений к календарю, но не мог понять, какие именно даты открывает бот. Оставалось только предполагать, что он методично нажимает «следующий месяц».

Позже выяснилось, что предположение было довольно точным.

Робот, который хотел увидеть будущее

Я изменил структуру URL и перенёс дату непосредственно в путь:

/calendar/{login}/{year}/{month}

Теперь адреса стали выглядеть так:

/calendar/chrono/2026/08/calendar/chrono/2027/01/calendar/chrono/2047/03

После публикации обновления в статистике начали появляться настоящие маршруты бота. Среди них был, например, такой:

/calendar/chrono/7271/12

А затем:

/calendar/chrono/7273/12

При этом номера месяцев всегда оставались корректными — от 1 до 12. То есть робот не подставлял случайные значения: он действительно следовал календарной навигации.

Возможно, часть ссылок уже находилась в его внутренней очереди, поэтому даже после обновления он ещё некоторое время продолжал обходить старые даты. Иногда отдельные месяцы посещались повторно:

/calendar/chrono/7273/12/calendar/chrono/7273/05/calendar/chrono/1182/07/calendar/chrono/1725/01

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

Впрочем, календарём робот не ограничился. Он также открывал:

/search/chrono/games/completed

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

Что именно было сломано

Формально календарь работал правильно. Следующий месяц после декабря 2026 года — январь 2027 года. Следующий после декабря 7272-го — январь 7273-го.

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

Для обычного человека календарь практически ограничен здравым смыслом. Никто не станет несколько часов нажимать стрелку перехода к следующему месяцу. У робота таких ограничений не было: безумие и отвага не позволили ему остановиться.

Если на странице существует ссылка, робот может:

  1. добавить её в очередь;

  2. открыть страницу;

  3. найти на ней следующую ссылку;

  4. повторять процесс до тех пор, пока ссылки не закончатся.

А они не заканчивались. Каждая страница календаря создавала следующую страницу календаря:

2026-08→ 2026-09→ 2026-10→ ...→ 7273-12

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

Неожиданный нагрузочный тест

88 632 запроса за сутки — это в среднем примерно:

3693 запроса в час61 запрос в минутуоколо одного запроса в секунду, но чаще всего двух, потому что другие роботы тоже прикладывали усилия

При этом проект продолжал работать без ошибок. Не появилось:

  • заметного роста нагрузки;

  • ошибок 500;

  • проблем с памятью;

  • зависших запросов;

  • сбоев базы данных.

Страница календаря оказалась достаточно лёгкой, а сервер спокойно пережил путешествие на пять тысяч лет вперёд.

Это не полноценный нагрузочный тест, но всё равно полезный результат. Я точно не планировал проверять проект таким способом сразу после запуска.

Как я исправил навигацию

Первым делом я оставил новые понятные URL:

/calendar/chrono/calendar/chrono/2026/calendar/chrono/2026/08

Теперь для внутренней аналитики достаточно сохранять обычный путь: год и месяц уже находятся внутри него.

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

То есть он может визуально оставаться стрелкой, но больше не является ссылкой:

<span class="calendar-arrow disabled">→</span>

Вместо:

<a href="/calendar/chrono/7274/01">→</a>

Это остановило автоматическое создание новых календарных маршрутов. При этом я пока сохранил возможность вручную открыть произвольный URL. Например:

/calendar/chrono/7273/12

Мне хотелось оставить такую возможность хотя бы для отладки. Если расчёт допустимого диапазона когда-нибудь окажется неправильным, нужная страница не будет полностью заблокирована.

Для неактуальных страниц я добавил:

<meta name="robots" content="noindex, follow">

Таким образом:

  • страница остаётся доступной по прямому адресу;

  • поисковику предлагается не добавлять её в индекс;

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

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

//chrono

Почему я не стал сразу возвращать 404

Самым строгим решением было бы возвращать 404 Not Found для дат за пределами реальной игровой истории пользователя.

Например:

abort_if(    $year < $firstActivityYear || $year > now()->year,    404);

Но на первом этапе я решил не делать ограничение жёстким. Причины довольно практичные:

  • расчёт первой и последней доступной даты может содержать ошибку;

  • произвольные даты полезны для ручного тестирования;

  • сама страница почти не нагружает сервер;

  • новые ссылки на несуществующие даты больше не генерируются;

  • такие страницы получают noindex.

Возможно, позже я всё же перейду на 404 или 410. Пока доступность страницы и её индексируемость разделены.

Что показала собственная аналитика

Google Analytics и Яндекс Метрика полезны для общей статистики, но в этой ситуации собственная таблица визитов оказалась гораздо нагляднее.

Я мог увидеть:

  • конкретный IP;

  • User-Agent;

  • последовательность запросов;

  • страницы, которые посещались повторно;

  • точные URL после изменения маршрутов;

  • момент, когда поведение робота изменилось.

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

После очистки старых записей картина стала гораздо реалистичнее: вместо бесконечного потока календарных запросов начали появляться обычные гости, которые открывали главную страницу и раздел «О проекте».

Чем всё закончилось

Я не стал превращать несуществующие даты в пустую техническую страницу или сразу возвращать 404.

Теперь, если открыть период, для которого у пользователя нет игровой истории, календарь объясняет, что записей за эту дату не найдено, и предлагает несколько полезных направлений:

  • перейти в профиль пользователя;

  • посмотреть его игровые коллекции;

  • открыть полную историю игр;

  • вернуться к сегодняшней дате в календаре.

Например, так выглядит календарь за несуществующий период:

https://gamestory.kz/calendar/chrono/2027

При этом страница получает:

<meta name="robots" content="noindex, follow">

Получился компромисс:

  • бессмысленные календарные периоды не должны попадать в поисковую выдачу;

  • робот всё ещё может обнаружить профиль, коллекции и другие полезные страницы;

  • пользователь не оказывается в тупике;

  • произвольный URL остаётся доступным для ручной проверки;

  • календарь больше не создаёт бесконечную цепочку новых ссылок.

После изменения навигации активность робота постепенно стабилизировалась. В статистике перестали доминировать десятки тысяч переходов по календарю, а среди посещений начали появляться обычные гости.

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

Какие выводы я сделал

1. Визуально отключённая ссылка всё ещё остаётся ссылкой

Недостаточно изменить цвет элемента или добавить класс disabled. Если у элемента остаётся href, робот всё ещё видит полноценный маршрут.

2. Любая навигация должна иметь естественные границы

Календари, пагинация, архивы и фильтры особенно легко создают бесконечные последовательности URL. Человек останавливается сам. Робот — необязательно.

3. Для аналитики полезно сохранять полный адрес

Если поведение страницы зависит от GET-параметров, одного $request->path() недостаточно.

Полезно хранить хотя бы отдельно:

$request->path();$request->getQueryString();$request->fullUrl();

Иначе тысячи разных страниц могут выглядеть как одна.

4. Боты иногда оказываются неплохими тестировщиками

Этот робот проверил:

  • календарную навигацию;

  • работу дат на тысячелетия вперёд;

  • стабильность Laravel-приложения;

  • производительность базы;

  • систему собственной аналитики.

И не создал ни одного баг-репорта.

5. Crawl budget можно потратить очень странным способом

Для небольшого сайта это вряд ли критично, но бессмысленные календарные URL отнимают у поискового робота время, которое он мог бы потратить на профили, списки и страницы игр.

Поэтому бесконечную цепочку всё равно стоило остановить, даже если сервер спокойно её выдерживал.

Итог

Я запускал сервис для хранения игровой истории, а получил историю о роботе, который отправился исследовать игровой календарь до 7273 года.

Баг оказался не в вычислении дат и не в Laravel. Каждая отдельная ссылка была корректной. Ошибка возникла из-за того, что последовательность корректных ссылок не имела конца.

После исправления робот постепенно успокоился, количество запросов стабилизировалось, а в аналитике наконец начали появляться обычные гости.

Теперь мне интересно мнение сообщества.

Какое поведение вы бы выбрали для календаря за пределами реальных данных пользователя?

  • жёсткий 404;

  • 410 Gone;

  • перенаправление на ближайший существующий период;

  • доступная страница с noindex;

  • другой вариант?

Сам проект можно посмотреть на gamestory.kz. Но главным результатом первого публичного запуска пока стал не новый пользователь, а очень целеустремлённый робот, который доказал: календарь действительно готов к долгосрочному хранению игровой истории.

ссылка на оригинал статьи https://habr.com/ru/articles/1068334/