ERR_CONNECTION_CLOSED и ERR_EMPTY_RESPONSE: сервер молча закрыл соединение
11 мин чтения
ERR_CONNECTION_CLOSED и ERR_EMPTY_RESPONSE: сервер молча закрыл соединение

ERR_CONNECTION_CLOSED и ERR_EMPTY_RESPONSE: сервер молча закрыл соединение

Соединение установилось, TLS прошёл, запрос ушёл — и в ответ ничего: ни кода 5xx, ни страницы ошибки, только «Сайт не отправил данных». Разбираем, чем эти коды отличаются друг от друга и от ERR_CONNECTION_RESET, какие директивы nginx, PHP-FPM и Apache к ним приводят и как найти виновника по curl, tcpdump и логам.

ERR_CONNECTION_CLOSED (код Chromium −100) означает, что сервер корректно закрыл TCP-соединение пакетом FIN, не дослав HTTP-ответ; ERR_EMPTY_RESPONSE (−324) — что соединение было закрыто, не отправив ни одного байта. В обоих случаях TCP-порт открыт и рукопожатие прошло, а отказ произошёл на уровне приложения: веб-сервер, прокси или защитный фильтр решили не отвечать. Этим они отличаются от ERR_CONNECTION_RESET, где соединение обрывается пакетом RST.

Три способа сервера прекратить разговор

У TCP ровно два механизма завершить соединение: FIN («у меня больше нет данных», RFC 9293, раздел 3.6) и RST («соединения нет или оно аварийно оборвано», раздел 3.5.2). Chromium различает их и добавляет третий случай — FIN на свежем соединении, когда не пришло ничего.

Код Chromium Что произошло на TCP curl В tcpdump Типичный виновник
ERR_EMPTY_RESPONSE (−324) Новое соединение, запрос ушёл, в ответ 0 байт и FIN «Empty reply from server», exit 52 [F.] от сервера сразу после запроса nginx return 444, антибот, упавший воркер
ERR_CONNECTION_CLOSED (−100) FIN на переиспользованном keep-alive-соединении или после части ответа «Connection died, retrying a fresh connect» либо exit 18/56 [F.] без данных или посреди тела ответа Гонка keep-alive, лимит соединений, обрыв на прокси
ERR_CONNECTION_RESET (−101) Пакет RST вместо FIN «Connection reset by peer», exit 56 Flags [R] или [R.] DPI, файрвол, reset_timedout_connection

Комментарии в net_error_list.h Chromium однозначны: CONNECTION_CLOSED — «corresponding to a TCP FIN», CONNECTION_RESET — «corresponding to a TCP RST», EMPTY_RESPONSE — «the server closed the connection without sending any data». RST чаще приходит из сети, а не от приложения, и разобран в статье про ERR_CONNECTION_RESET; здесь — только про вежливое закрытие.

Почему одна и та же проблема даёт то CLOSED, то EMPTY_RESPONSE

Разницу определяет не сервер, а то, было ли соединение новым. В http_stream_parser.cc Chromium: сокет закрылся с нулём принятых байт на новом соединении — ERR_EMPTY_RESPONSE; на переиспользованном — ERR_CONNECTION_CLOSED, потому что «сервер, скорее всего, закрыл сокет до того, как получил запрос».

Дальше включается повтор. HandleIOError в http_network_transaction.cc повторяет запрос при CONNECTION_CLOSED, CONNECTION_RESET, CONNECTION_ABORTED, SOCKET_NOT_CONNECTED и EMPTY_RESPONSE, но только если соединение уже использовалось (keep-alive) и заголовки ответа не начали приходить; лимит — 50 повторов, дальше ERR_TOO_MANY_RETRIES. На свежем соединении повтора нет. Правило: EMPTY_RESPONSE — почти всегда стабильная ошибка конфигурации или фильтра, CONNECTION_CLOSED — чаще гонка, перегрузка или обрыв посреди ответа.

Что на сервере закрывает соединение без ответа

Почти всегда это одно из десяти мест ниже, и у каждого есть директива, проверяемая за минуту. Умолчания — по документации nginx, Apache и PHP-FPM на сентябрь 2026.

Причина Директива / умолчание Как проверить
nginx намеренно молчит return 444 — «закрывает соединение без отправки заголовка ответа» grep -rn 444 /etc/nginx/; статус 444 в access.log
Гонка keep-alive keepalive_timeout 75s, keepalive_requests 1000 (до 1.19.10 — 100); Apache KeepAliveTimeout 5 Плавающая ошибка со второго запроса; таймаут бэкенда меньше таймаута прокси
Исчерпаны соединения nginx worker_connections 512 (считает и соединения к бэкенду), worker_rlimit_nofile error.log: «worker_connections are not enough», «Too many open files»; ss -s
Исчерпаны воркеры бэкенда pm.max_children (обязательный, умолчания нет); Apache MaxRequestWorkers 256 php-fpm.log: «server reached pm.max_children setting»; страница pm.status_path
Воркер убит по таймауту request_terminate_timeout (0 = выкл.) php-fpm.log: «execution timed out (…), terminating»
Падение процесса segfault, OOM-killer error.log: «worker process … exited on signal 11»; dmesg: «Out of memory: Killed process»
Слишком большие заголовки large_client_header_buffers 4 8k; Apache LimitRequestFieldSize 8190 nginx отдаёт 400 (внутренний код 494 «Request Header Or Cookie Too Large»); молча рвут некоторые прокси; Cloudflare при 128 КБ даёт 520
HTTP/2 GOAWAY http2_max_concurrent_streams 128, keepalive_timeout для h2 chrome://net-export; curl --http1.1 работает, curl --http2 — нет
WAF, антибот, ModSecurity ModSecurity: действие drop вместо deny; кастомные правила антибота Воспроизводится только с определённым User-Agent или IP
Нет подходящего vhost / SNI ssl_reject_handshake on; default_server с return 444 curl -v --resolve с правильным именем работает, по IP — нет

Отдельно про PHP-FPM за nginx. Когда FPM-воркер умирает или убит по request_terminate_timeout, nginx пишет «upstream prematurely closed connection while reading response header from upstream» и отдаёт 502 Bad Gateway — браузер видит код, а не пустоту. Пустой ответ приходит, когда рвётся сам nginx (упал воркер, кончились worker_connections) или когда приложение отвечает без прокси: Node или Go-сервер на 0.0.0.0:8080 при падении просто закрывает сокеты.

Что означает Cloudflare 520 и как он связан с пустым ответом

Cloudflare 520 «Web server returns an unknown error» — тот же ERR_EMPTY_RESPONSE, только увиденный не браузером, а прокси Cloudflare. По документации Cloudflare 520 возникает, когда origin «возвращает пустой, неизвестный или неожиданный ответ»: сбой сервера, блокировка IP-адресов Cloudflare файрволом origin, заголовки больше 128 КБ (обычно из-за cookie), некорректная настройка HTTP/2 к origin.

Код Cloudflare Что видел Cloudflare от origin Аналог без CDN
520 Соединение закрыто без валидного HTTP-ответа ERR_EMPTY_RESPONSE / ERR_CONNECTION_CLOSED
521 Origin отказал в TCP-соединении ERR_CONNECTION_REFUSED
522 TCP-рукопожатие не завершилось ERR_CONNECTION_TIMED_OUT
524 HTTP-ответ не пришёл за лимит времени ERR_TIMED_OUT / 504

Диагностика та же, но в обход Cloudflare: curl --resolve на IP origin покажет, действительно ли сервер отдаёт пустоту. Если напрямую всё работает, проверяют, не режет ли файрвол или антибот диапазоны Cloudflare и не включён ли HTTP/2 to Origin при бэкенде без его поддержки.

Когда виноват не сервер: клиент и сеть

Если ошибка только у одного пользователя, а с других устройств сайт открывается — источник между браузером и сервером. Антивирус с проверкой HTTPS, корпоративный прокси, VPN-клиент и расширения-блокировщики вклиниваются в соединение и могут закрыть его без HTTP-ответа; DPI провайдера шлёт RST, то есть даёт RESET, а не CLOSED. Тест: тот же URL в инкогнито, без VPN и веб-фильтра антивируса, затем с мобильного интернета. Исчезло — дело в локальной цепочке.

Диагностика: curl, tcpdump и логи

Первый инструмент — curl -v: видно, ушёл ли запрос и что пришло после. Пустой ответ на новом соединении (вывод — пример):

$ curl -sv https://example.ru/ -o /dev/null; echo "exit=$?"
*   Trying 203.0.113.10:443...
* Connected to example.ru (203.0.113.10) port 443
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* using HTTP/1.x
> GET / HTTP/1.1
> Host: example.ru
> User-Agent: curl/8.10.1
>
* Empty reply from server
* Closing connection
curl: (52) Empty reply from server
exit=52

# Сравнить протоколы: с --http1.1 ответ есть, с --http2 нет — сломан h2 на сервере или прокси
$ curl -s -o /dev/null -w '%{http_code} %{http_version}\n' --http1.1 https://example.ru/
$ curl -s -o /dev/null -w '%{http_code} %{http_version}\n' --http2   https://example.ru/
# Обойти CDN и проверить origin напрямую
$ curl -sv --resolve example.ru:443:203.0.113.10 https://example.ru/ -o /dev/null

Коды выхода curl: 52 (CURLE_GOT_NOTHING) — «nothing was returned from the server», 56 (CURLE_RECV_ERROR) — ошибка приёма, чаще всего RST, 18 (CURLE_PARTIAL_FILE) — сервер обещал Content-Length и закрыл соединение раньше, 16 (CURLE_HTTP2) — ошибка в кадрах HTTP/2. На переиспользованном соединении curl, как и Chrome, повторяет запрос: в verbose появится «Connection died, retrying a fresh connect», лимит — 5 попыток.

Второй — tcpdump на сервере: он однозначно отвечает, FIN это или RST и кто его отправил.

# Только пакеты с FIN или RST на 443-м порту
sudo tcpdump -ni eth0 'port 443 and (tcp[tcpflags] & (tcp-fin|tcp-rst) != 0)'

# Пример: сервер закрыл соединение FIN-ом через 40 мс после запроса, не отправив данных
12:41:07.118 IP 198.51.100.7.51234 > 203.0.113.10.443: Flags [P.], seq 1:518, length 517
12:41:07.158 IP 203.0.113.10.443 > 198.51.100.7.51234: Flags [F.], seq 1, ack 518, length 0
12:41:07.159 IP 198.51.100.7.51234 > 203.0.113.10.443: Flags [F.], seq 518, ack 2, length 0

Flags [F.] от адреса сервера с length 0 сразу после запроса — и есть пустой ответ. Flags [R.] вместо него — читайте статью про RESET. FIN после нескольких пакетов с данными — обрыв посреди тела: в браузере тоже CONNECTION_CLOSED, в curl — exit 18.

Третий инструмент — логи: ошибки этого класса оставляют характерные строки.

sudo tail -n 200 /var/log/nginx/error.log | grep -E 'prematurely|exited on signal|not enough|no live|Too many open'
sudo grep -E 'max_children|timed out|exited on signal' /var/log/php8.3-fpm.log | tail
sudo dmesg -T | grep -i -E 'out of memory|killed process'

ss -s                      # сводка: сколько соединений всего и в каких состояниях
ss -tn state established '( sport = :443 )' | wc -l
curl -s localhost/fpm-status # active processes, listen queue, max children reached
Строка в логе Причина Исправление
upstream prematurely closed connection while reading response header Бэкенд закрыл сокет до ответа: таймаут, падение, перезапуск Смотреть лог бэкенда; поднять request_terminate_timeout или ускорить код
worker process 1234 exited on signal 11 Segfault воркера nginx: модуль, Lua, битая сборка Отключать сторонние модули по одному, обновить nginx
512 worker_connections are not enough Лимит соединений на воркер исчерпан worker_connections 4096 плюс worker_rlimit_nofile 8192
no live upstreams while connecting to upstream Все бэкенды помечены недоступными после серии ошибок Чинить бэкенд; проверить max_fails и fail_timeout
[pool www] server reached pm.max_children setting (5) Все FPM-воркеры заняты, запросы копятся в очереди Поднять pm.max_children под объём памяти
child 4321 … execution timed out (30.2 sec), terminating Сработал request_terminate_timeout Ускорить скрипт или вынести задачу в фон
Out of memory: Killed process 4321 (php-fpm8.3) OOM-killer снял процесс с клиентом на руках Уменьшить pm.max_children × memory_limit, добавить память
access.log: "GET /wp-login.php" 444 0 Правило с return 444 сработало на легитимный запрос Сузить условие location/if

Как исправить: по стекам

У каждого стека три-четыре ручки. Главный принцип для цепочки прокси → бэкенд: таймаут простоя keep-alive у бэкенда должен быть больше, чем у прокси перед ним, иначе бэкенд закроет соединение ровно в момент, когда прокси решит его переиспользовать.

Стек Что проверить Типовое исправление
nginx return 444, worker_connections, keepalive_timeout, large_client_header_buffers, http2 Ограничить 444 явными условиями; worker_connections 4096; у бэкенда keepalive_timeout выше, чем у фронта
Apache MaxRequestWorkers 256, KeepAliveTimeout 5, MaxKeepAliveRequests 100, ModSecurity drop Поднять MaxRequestWorkers вместе с ServerLimit; вместо drop — deny,status:403
PHP-FPM pm.max_children, request_terminate_timeout, pm.max_requests, память max_children = свободная память ÷ средний RSS воркера; включить pm.status_path и следить за listen queue
Node / PM2 server.keepAliveTimeout (5 с в выпущенных версиях), необработанные исключения, рестарты PM2 keepAliveTimeout больше idle-таймаута балансировщика; логировать uncaughtException; graceful shutdown вместо kill
Cloudflare 520 в логах, файрвол origin, размер cookie, HTTP/2 to Origin Разрешить IP-диапазоны Cloudflare на origin; урезать cookie; отключить HTTP/2 to Origin

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

worker_rlimit_nofile 16384;
events { worker_connections 8192; }
http {
    keepalive_timeout 75s;              # фронт
    large_client_header_buffers 4 16k;  # если 400/494 на больших cookie
    upstream app { server 127.0.0.1:3000; keepalive 32; }
    server { location / {
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_next_upstream error timeout http_502;
        proxy_pass http://app;
    } }
}
# Бэкенд (Node): server.keepAliveTimeout = 90_000 — больше 75 с у nginx
return 444 — не защита, а способ спрятать проблему

Правила вида «User-Agent не похож на браузер — 444» регулярно отсекают платёжные шлюзы, вебхуки, краулеры и мониторинг. В логах это 444 и 0 байт, у второй стороны — ERR_EMPTY_RESPONSE без объяснений. Если фильтр нужен, отдавайте 403 с телом. Как фильтры ведут себя с проверками, разобрано в статье о WAF и мониторинге.

Как узнать об обрывах раньше пользователей

В access.log этих ошибок часто нет: запрос без ответа может не оставить ни строки, а «upstream prematurely closed» видно только в error.log. Снаружи обрыв обнаруживается одним способом — регулярным HTTP-запросом с разных точек, который ждёт валидный ответ, а не просто открытый порт.

HTTP-проверка выполняет полноценный GET, и пустой ответ или обрыв соединения фиксируется как недоступность так же, как 5xx: критерий — «получен ожидаемый ответ», а не «порт открыт». Параллельная проверка TCP-порта в тот же момент показывает, что 443-й порт по-прежнему принимает соединения. Сочетание «TCP зелёный, HTTP красный» — подпись ERR_EMPTY_RESPONSE: сервер жив, приложение или фильтр не отвечает. Вкладка «По локациям» покажет, рвётся у всех или только из одного региона.

Обрыв без кода ответа — тоже инцидент

Пруфен проверяет сайт по HTTP с интервалом от 2 минут из 13 точек мониторинга — 11 в России, а также Минск и Испания — и присылает уведомление в Email, Telegram или Макс, когда вместо страницы приходит пустой ответ или закрытое соединение. Если ресурс так и не поднялся, подключается голосовой звонок. В бесплатном плане — 5 проверок.

Частые вопросы

Чем ERR_EMPTY_RESPONSE отличается от 502 Bad Gateway?

502 — полноценный HTTP-ответ: прокси (nginx, балансировщик, CDN) не получил ответа от бэкенда и сообщил об этом кодом и страницей. ERR_EMPTY_RESPONSE означает, что HTTP-ответа не было вовсе — соединение закрылось на уровне TCP. 502 говорит «сломан бэкенд за прокси», пустой ответ — «молчит тот, к кому вы подключились напрямую». Если перед сайтом стоит nginx и вы видите пустоту, а не 502, проблема в самом nginx или раньше.

Почему curl показывает «Empty reply from server», а браузер иногда открывает страницу?

Три причины. Антибот или правило nginx реагирует на User-Agent curl — проверьте с ключом -A и заголовками из браузера. Протокол: браузер ходит по HTTP/2, curl может выбрать HTTP/1.1, и сломан только один из них. Повтор: на keep-alive-соединении Chrome молча повторяет запрос до 50 раз, а curl на новом соединении сразу получает exit 52.

Может ли антивирус давать ERR_CONNECTION_CLOSED?

Может. Модули проверки HTTPS-трафика работают как локальный прокси: браузер соединяется с ними, а не с сайтом. Если модуль блокирует страницу или падает, браузер видит закрытое соединение без ответа при исправном сервере. Признак: ошибка только на одном компьютере и исчезает при отключении веб-защиты.

Что такое nginx 444 и зачем он нужен?

444 — нестандартный внутренний код nginx: по документации, он «закрывает соединение без отправки заголовка ответа». В сеть код не уходит — клиент получает только FIN, в access.log остаётся 444 и 0 байт. Используют его против мусорных запросов: обращений по IP без Host, сканеров, default_server для чужих доменов. С reset_timedout_connection on такие соединения закрываются RST-ом, и CLOSED у клиента превращается в RESET.

Была ли статья полезна?

Начните мониторинг прямо сейчас

Бесплатный тариф — до 5 проверок. 150 ₽ на баланс при регистрации.

Создать аккаунт