Однажды в логах сборщика данных появилась строка «не удалось получить данные:» — и дальше пустота. Разработчик годами считал это багом форматирования, пока не выяснил: код написан правильно, просто у asyncio.TimeoutError пустой str(). Эта история — отличный пример того, как пустое сообщение об ошибке может запустить цепочку неверных решений, включая архитектурные. Разберём, что произошло, и как избежать подобных ловушек.
Ситуация усугубилась тем, что рядом лежал хелпер с тремя попытками и паузой в пять секунд, написанный специально для сетевых сбоев. Но ловил он except aiohttp.ClientError. Начиная с Python 3.11, asyncio.TimeoutError — это встроенный TimeoutError, наследник OSError, и к иерархии aiohttp он отношения не имеет. То есть единственная ошибка, ради которой ретраи существовали, пролетала мимо них. При этом у aiohttp есть свои таймаутные исключения, которые как раз ClientError, — и это сбивает с толку.
Почему сообщение об ошибке оказалось пустым
Многие исключения в Python имеют осмысленное строковое представление, но asyncio.TimeoutError (а с 3.11 — TimeoutError) не передаёт в конструктор никаких аргументов. Поэтому str(e) возвращает пустую строку. В логе появляется «не удалось получить данные:» и ничего после двоеточия. Это не баг форматирования, а особенность исключения. Если вы логируете только str(e), вы теряете информацию о типе ошибки и контексте.
Решение простое: логируйте repr(e) или явно указывайте тип исключения. Например, logger.error('Ошибка: %r', e) даст TimeoutError(), что сразу прояснит ситуацию. Также полезно добавлять контекст: какой URL запрашивался, сколько времени ждали, какие настройки таймаута.
Как неправильный перехват исключений ломает ретраи
Хелпер с тремя попытками был написан для обработки сетевых сбоев, но ловил только aiohttp.ClientError. В то же время asyncio.TimeoutError (он же TimeoutError) не является наследником ClientError. Поэтому таймауты не попадали в обработчик и не вызывали повторных попыток. Разработчик был уверен, что ретраи работают, но на самом деле они не срабатывали для самой частой проблемы — таймаутов.
- Типичная ошибка: перехватывать слишком узкий класс исключений, не учитывая встроенные таймауты.
- Как исправить: явно добавлять except (aiohttp.ClientError, asyncio.TimeoutError) или использовать более широкий except (aiohttp.ClientError, OSError), так как TimeoutError наследуется от OSError.
- Проверка: напишите тест, который имитирует таймаут, и убедитесь, что ретраи срабатывают.
Кроме того, стоит помнить, что у aiohttp есть собственные исключения таймаутов, например aiohttp.ServerTimeoutError, которые являются подклассами ClientError. Их нужно обрабатывать отдельно или включать в общий перехват.
Как пустая ошибка привела к ложному архитектурному выводу
Из-за отсутствия информации в логах разработчик предположил, что проблема в сетевых ограничениях VPS. Он записал в заметки «сетевое ограничение VPS» и построил вокруг этого архитектурное решение — например, добавил дополнительные прокси или изменил схему развёртывания. На самом деле никаких сетевых ограничений не было, а ошибка была вызвана таймаутом, который не обрабатывался ретраями. Это классический пример того, как недостаток диагностики приводит к дорогостоящим и ненужным изменениям.
Чтобы избежать такого:
- Всегда логируйте тип исключения и repr(e), а не только str(e).
- Добавляйте в лог контекст: URL, параметры запроса, время ожидания.
- Проверяйте гипотезы экспериментом, а не стройте архитектуру на догадках.
- Используйте мониторинг и алерты по количеству ошибок, чтобы вовремя замечать аномалии.
Практические шаги для надёжной обработки ошибок
Вот конкретные действия, которые помогут избежать подобных ситуаций в вашем проекте:
- Изучите иерархию исключений используемых библиотек. Для asyncio и aiohttp составьте список таймаутных исключений и их базовых классов.
- Настройте логирование так, чтобы в сообщении всегда был тип ошибки. Например, используйте формат %(exc_info)s или repr(e).
- Пишите тесты на обработку ошибок: имитируйте таймауты, сетевые сбои и проверяйте, что ретраи срабатывают.
- Пересмотрите существующие обработчики: добавьте недостающие классы исключений, особенно asyncio.TimeoutError и OSError.
- Документируйте предположения: если вы делаете архитектурный вывод на основе логов, убедитесь, что он подтверждён экспериментально.
История с пустой ошибкой — напоминание о том, что дьявол в деталях. Пустое сообщение может скрывать за собой важную информацию, а неверная интерпретация — приводить к лишним затратам. Внимательность к типам исключений и контексту логирования помогает принимать верные решения и не строить воздушные замки.