Что было. Два сервиса на Node.js общаются по HTTP с постоянными соединениями. Раз в несколько тысяч запросов клиент получает ECONNRESET или socket hang up, хотя сервер жив и ничего не логирует. Воспроизводится стабильно, если посылать запрос ровно раз в 5 секунд.
Почему. У сервера есть таймаут простоя keep-alive соединения (в Node.js по умолчанию 5 секунд, server.keepAliveTimeout). Когда он истекает, сервер закрывает сокет. Пакет FIN идёт к клиенту несколько миллисекунд, и если клиент в это окно уже отправил новый запрос в «живой» с его точки зрения сокет, ответом будет сброс. Это не ошибка реализации, а свойство протокола; обращение закрыли как ожидаемое поведение и перечислили меры.
Как закрыть гонку.
- Таймаут простоя у клиента короче, чем у сервера. Клиент должен сам закрывать соединение раньше, чем это сделает сервер, тогда он никогда не отправит запрос в умирающий сокет. В Node.js: агент с
keepAlive: trueиfreeSocketTimeout(вagentkeepalive) илиkeepAliveTimeoutв undici на 1–2 секунды меньше серверного; в GoIdleConnTimeoutуTransport; в Pythonrequestsчерез адаптер с пулом иpool_maxsize, таймаут на стороне сервера длиннее. - Таймаут у сервера длиннее.
server.keepAliveTimeout = 65000(иheadersTimeoutбольше него) на сервере, если перед ним nginx сkeepalive_timeout 60sв upstream: правило то же, сервер закрывает позже клиента. - Повтор идемпотентных запросов. GET/HEAD/PUT/DELETE можно безопасно повторить при
ECONNRESETна первой же попытке отправки; для POST повтор допустим только с ключом идемпотентности.
Та же гонка с nginx посередине.
- nginx → приложение:
upstream { keepalive 32; }плюсproxy_http_version 1.1; proxy_set_header Connection "";иkeepalive_timeoutв upstream меньше, чемkeepAliveTimeoutприложения. Иначе вerror.logпоявляетсяupstream prematurely closed connection/recv() failed (104: Connection reset by peer). - клиент → nginx:
keepalive_timeout 65;на стороне nginx больше, чем таймаут у клиентов (браузеры и большинство HTTP-библиотек закрывают простаивающие соединения раньше минуты).
Как убедиться, что это ваш случай. Ошибки редкие, распределены случайно, чаще при низкой нагрузке (сокеты успевают простоять до таймаута), исчезают при keepAlive: false. Если ошибки массовые или при пиковой нагрузке, причина другая: лимиты дескрипторов, падения воркеров, таймауты запросов.
Параметр описан в документации Node.js.