← На главную

Сервер сбрасывает соединение из-за оставшихся данных в сокете

17.05.2026 17:09 · hackernews

Разработчик столкнулся с загадочными сбоями при передаче данных между двумя сервисами на одном компьютере. Сервер открывает слушающий сокет и в ответ на подключение сразу отправляет клиенту 600 000 байт информации. Клиент должен прочитать весь поток, но иногда в процессе получения данных вызов recv() возвращает ошибку ECONNRESET со значением errno 104, даже если никаких аварий или других проблем в логах нет. Проблема проявляется нестабильно: иногда клиент получает полный объем данных, а в других случаях связь обрывается на разной отметке — от 100 до 600 тысяч байт.

Исследование с помощью утилит strace показало, что сброс соединения инициирует именно сервер. В его процессе после отправки данных и выполнения sendto() идет вызов close() для сокета. Как только происходит закрывание, происходит генерация TCP RST. Если не задерживать это действие, ошибка неизбежна при наличии недочитанных данных на сокете сервера. Смысл в том, что при закрытии «грязного» сокета, где остались буферизованные пакеты, система пытается сообщить клиенту, что не все данные были прочитаны, и разрывает канал.

В реальной ситуации такая проблема возникла у gunicorn в связке с flask, который работал за прокси-сервером nginx. Запрос от клиента приходит через writev() с заголовками и телом, а gunicorn считывает их порциями. Иногда стек работает лениво: приложениe обрабатывает только заголовки и игнорирует тело запроса, не вызывая recv() для прочтения всего содержимого. Однако перед отправкой ответа gunicorn вынужден закрыть сокет. Если внутри есть недочитанные данные, это провоцирует повторение ECONNRESET.

Решение оказалось простым, но рискованным: в коде Python-приложения нужно выполнять dummy-операцию над телом HTTP-запроса, гарантируя полное чтение данных с сокета, прежде чем закрывать соединение. Это убирает проблему, но теоретически открывает вектор для атак типа DoS, если злоумышленник может отправить огромный файл, заставляя приложение тратить память на его буферизацию, если лимит client_max_body_size в nginx не настроен. Дальнейшие шаги включают поиск официального источника описываемой поведенческой модели в Linux и проверку того, является ли вина gunicorn, flask или самого приложения.

Читать оригинал →