Есть класс багов, которые живут месяцами именно потому, что они ничьи. Разработчики смотрят в код: код чистый. Админы смотрят в сервер: сервер отдаёт файлы как положено. Баг сидит на стыке и пересиживает обоих. Здесь таким стыком оказались два байта.
Сводка
| Отрасль конечного клиента | детский отдых: сайт лагеря с онлайн-заказом путёвок |
| Конечный клиент | клиент студии; админка отчётов по сменам |
| Формат сотрудничества | абонентская DevOps-поддержка студии: трекер + общий чат |
| Тип проекта | диагностика прикладного бага на стыке кода и сервера |
| Объём работ | 1 сервис отчётов (Yii 2 / PHP 7.4), 1 сервер (nginx + Apache) |
| Дата проекта | 5 сен 2024 – 11 сен 2024 (6 дней) |
| Трудозатраты | 1 ч по трекеру |
| Команда | 2 специалиста (инженер-сисадмин · руководитель проекта) |
| Технологический стек | Yii 2 · PHP 7.4 · nginx · Apache · dd / xxd / file |
| Сдано | причина найдена за вечер: 2 лишних байта CRLF из кода; обходная отдача файлов работает с 11.09.2024 |
Постановка задачи
В начале 2024 года студия перевезла сайты своего клиента, детского лагеря, с чужого хостинга на собственный сервер. После переезда в админке заказов сломалась одна функция: отчёты по сменам. Кнопка «сгенерировать документ» отдаёт Excel-файл, который не открывается. Тот же отчёт, сохранённый на диск сервера и забранный по FTP, открывается без единой ошибки.
Запрос пришёл словами клиента, без прикрас: «Мы в коде не нашли никаких проблем, и складывается впечатление, что он портится где-то в момент скачивания в браузер. То ли nginx его коверкает при отдаче, то ли ещё чего. Сможете посмотреть, проанализировать?» К этому моменту проблема тянулась с февраля — семь месяцев. Сотрудникам лагеря выдали FTP-доступ, и отчёты они забирали вручную с сервера. Жить можно, работать неудобно.
Что в этом сложного
Такой баг дорог не часами работы, а тем, сколько он висит. Он не роняет сайт, поэтому никогда не попадёт в очередь срочных аварий. Разработчики проверили свою зону и честно ничего не нашли; на старом хостинге тот же код работал, значит «виноват сервер»; админ смотрит на сервер: сервер штатный. Каждый прав, а файл битый. Из этой точки у студии обычно два пути: переписывать выгрузку наугад или возить отчёты по FTP вечно. Тому, кто платит за поддержку, нужен третий: локализовать виновный слой за конечное, заранее понятное время.
Как мы это сделали
1. Сначала факт, потом гипотезы. Взяли два экземпляра одного отчёта: скачанный браузером и его близнец с диска сервера. Скачанный оказался на 2 байта длиннее. Отрезали эти 2 байта через dd ... bs=1 skip=2 — и «битый» файл открылся. Утилита file подтвердила: усечённая копия — Microsoft Excel 2007+, оригинал — безликая data. Сами байты вытащили и посмотрели в xxd: 0d0a. Это CRLF, перевод строки. Формулировка задачи сжалась с «где-то что-то портится» до «кто-то дописывает rn перед телом файла».
2. Слой-виновник искали контрольными отдачами, а не спорами. Гипотезу про nginx (сжатие, правка ответа) инженер снял сразу, по опыту: ничего подобного nginx с телом ответа не делает, что и подтвердилось дальше. С Apache — та же история. Решающих тестов было два. Тот же код отдачи на соседнем проекте этого же сервера, но на PHP 8.2, вернул файл целым. А простой PHP-скрипт в три строки, который забирает готовый XLSX с диска и отдаёт его мимо фреймворка, вернул целый файл с той же самой площадки. Диск чист, веб-серверы чисты, PHP чист. Остался один подозреваемый — цепочка отдачи Yii.
3. Серия «не сработало» — тоже результат. Параллельно разработчики студии проверяли точечные гипотезы. MIME-тип application/zip вместо xlsx — мимо. Слэш в Content-Disposition (полный путь вместо имени файла; нашли попутно, поправили): имя при скачивании стало нормальным, файл всё ещё битый. sendFile() вообще без ручных заголовков: битый. auto_detect_line_endings в настройках PHP — мимо. Каждый промах сужал круг: дело не в заголовках и не в конфигурации, CRLF впрыскивается где-то между отправкой заголовков и телом файла.
4. Решение выбрали по цене вопроса. Можно было копать внутренности фреймворка на PHP 7.4 в поисках строчки, которая печатает лишний перевод строки. Вместо этого инженер предложил обход: отдельный скрипт-прокладка вне Yii, который по идентификатору забирает готовый файл с диска и отдаёт его браузеру, а интерфейс выдаёт ссылку на прокладку. И назвал вещи своими именами: «костыль, да. но работать будет». Через 5 дней разработчики подтвердили в чате: «Костылик рабочий))». Отчёты снова скачиваются из админки, FTP-карусель закончилась.
Инциденты и реакция
Простой — ноль. Диагностика шла на работающей админке: эксперименты гоняли на тестовой смене, не трогая боевые заказы, а выданный для проверки доступ инженер вернул в тот же день, сам напомнив: «доступ в админку можно убирать». Деструктивных действий на сервере не было вовсе: вся побайтовая хирургия делалась на копиях файлов.
Результаты
| Метрика | Значение |
|---|---|
| Возраст проблемы к моменту обращения | ~7 месяцев (с февраля 2024) |
| Время до локализации слоя-виновника | один вечер: взято в работу в 15:30, к 18:39 виновник определён |
| Трудозатраты по трекеру | 1 ч |
| Физический размер дефекта | 2 байта (0d0a, CRLF) перед телом XLSX |
| Исключено слоёв | диск → nginx → Apache → версия PHP → фреймворк |
| Статус | обходная отдача в работе с 11.09.2024, подтверждена разработчиками |
Простыми словами: семь месяцев сервис отдавал нечитаемые отчёты, и никто не мог сказать, чья это проблема. За один вечер вопрос закрылся: испорченные файлы отличаются от здоровых ровно двумя байтами, дописывает их код, а не сервер, и пока разработчики решают, чинить ли цепочку отдачи фреймворка, отчёты скачиваются через прокладку. У клиента студии снова работает админка. У студии — аргументированный ответ, что чинить дальше.
Команда
- инженер-сисадмин (бюро) — побайтовая диагностика, исключение слоёв, схема обходной отдачи
- руководитель проекта (бюро) — параллельные гипотезы (сжатие nginx,
auto_detect_line_endings), координация с разработчиками студии
Проверки внутри кода вели разработчики на стороне клиента: диагностика шла в четыре руки, в одном чате, в один вечер.
Антон Херсун, Xaver Pro — руководитель проекта.
Похожая история — файл «портится непонятно где», а сервис месяцами живёт на обходном пути? Пришлите описание симптомов. Посмотрим, назовём виновный слой и вернёмся с оценкой в часах. Разбор — бесплатно.