Веб-серверы 12 мин чтения 2026-10-05

Проксирование WebSocket в Nginx: настройка заголовков Upgrade, Connection и устранение ошибки 1006 на VDS

Разбираем механику проксирования WebSocket в Nginx: переход HTTP 101, директиву map для Connection Upgrade, тайм-ауты proxy_read_timeout, WSS и устранение ошибки 1006.

Алексей Иванов Проверено инженерами SysKit.ru

1. Анатомия рукопожатия WebSocket и почему Nginx сбрасывает соединение

Протокол WebSocket (RFC 6455) обеспечивает полнодуплексный двунаправленный обмен сообщениями между клиентом и сервером поверх единого постоянного TCP-соединения. Он лежит в основе современных веб-чатов, дашбордов с живыми котировками и графиками, систем мгновенных push-уведомлений, совместных редакторов и ботов.

Установление WebSocket-сессии всегда начинается с обычного HTTP-запроса, называемого рукопожатием (WebSocket Handshake). Клиент отправляет стандартный запрос методом GET со специальными заголовками согласования:

GET /ws/ HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

Если бэкенд готов перейти на протокол веб-сокетов, он возвращает ответ со статусом HTTP 101 Switching Protocols:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

С этой секунды TCP-сокет перестает быть HTTP-каналом: клиент и сервер начинают обмениваться легковесными бинарными или текстовыми фреймами с минимальным оверхедом (всего 2–10 байт на сообщение вместо сотен байт HTTP-заголовков).

⚠️ Почему Nginx «из коробки» ломает веб-сокеты

В конфигурации по умолчанию модуль ngx_http_proxy_module в Nginx работает с вышестоящими серверами (апстримами) по протоколу HTTP/1.0. Кроме того, согласно стандартам RFC 2616 и RFC 7230, Nginx автоматически вырезает так называемые hop-by-hop заголовки, включая Upgrade и Connection. В результате бэкенд получает обычный урезанный HTTP/1.0 запрос, не понимает, что от него требуется рукопожатие сокета, и возвращает ошибку 400 Bad Request либо статус 200 OK с отдачей статической страницы.

Чтобы проверить, какие заголовки фактически возвращает ваш сервер при попытке подключения, воспользуйтесь онлайн-инструментом Анализ HTTP-заголовков онлайн.

2. Директива map $http_upgrade: безопасное переключение протоколов

Самая распространенная ошибка начинающих администраторов — жестко прописать в конфигурации проксирующего блока:

# Ошибочный подход: жесткая фиксация заголовка Connection
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

Такая запись приводит к скрытым багам: если к тому же эндпоинту или виртуальному хосту обратится обычный REST API или браузер со стандартным HTTP-запросом (где заголовок Upgrade отсутствует), Nginx все равно принудительно отправит бэкенду заголовок Connection: upgrade. Многие бэкенд-фреймворки (FastAPI, Node.js, Go) расценивают такой запрос как некорректный и аварийно завершают обработку с ошибкой 400.

Официальный стандарт Nginx требует использования динамической карты map в контексте http:

# Файл /etc/nginx/nginx.conf (внутри блока http { ... })
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

💡 Как работает карта переменных $connection_upgrade

Директива map анализирует значение входящей переменной $http_upgrade:
1. Если клиент прислал заголовок Upgrade: websocket, переменная $connection_upgrade принимает значение upgrade.
2. Если клиент отправил обычный HTTP-запрос (заголовок пуст: ''), переменная принимает значение close (или стандартное поддержание сессии).
Это гарантирует, что один и тот же location может абсолютно безопасно обрабатывать как сокеты, так и регулярные HTTP/API запросы.

3. Конфигурация виртуального хоста Nginx для проксирования сокетов

Объединим директиву map и параметры обратного проксирования в боевой конфигурации виртуального хоста Nginx.

Откройте файл конфигурации сайта (например, /etc/nginx/sites-available/app.conf):

# 1. Задаем пул upstream-серверов или локальный порт приложения
upstream websocket_backend {
    # Приложение на Node.js, Python, Go или Laravel Reverb
    server 127.0.0.1:3000;
    keepalive 32;
}

server {
    listen 80;
    server_name app.example.com;

    # Основной эндпоинт приложения или выделенный путь сокетов /ws/
    location /ws/ {
        proxy_pass http://websocket_backend;

        # Обязательный перевод проксирования на HTTP/1.1
        proxy_http_version 1.1;

        # Проброс динамически согласованных заголовков рукопожатия
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        # Проброс контекста клиента для авторизации и логирования
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Отключение буферизации для минимизации задержек доставки сообщений
        proxy_buffering off;

        # Увеличение таймаутов для предотвращения разрыва молчащих сессий
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
    }

    # Регулярные веб-страницы и статика
    location / {
        proxy_pass http://websocket_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

💡 Почему директива proxy_buffering off критична для сокетов

По умолчанию Nginx пытается буферизовать входящие и исходящие пакеты проксируемого сервера. Для интерактивных чатов, торговых сигналов или онлайн-игр буферизация недопустима: директива proxy_buffering off отключает задержку и заставляет Nginx мгновенно транслировать фреймы сокета напрямую клиенту в режиме реального времени.

После редактирования проверьте синтаксис и мягко перезагрузите Nginx:

# Тест синтаксиса конфигурации Nginx
sudo nginx -t

# Применение новых правил без сброса активных сессий
sudo systemctl reload nginx

Готовые проверенные шаблоны конфигураций для популярных стеков доступны в наших pSEO-инструментах: Конфигурация Nginx для Laravel с WebSocket (Reverb / Echo) и Nginx Reverse Proxy для Node.js и Next.js с WebSocket.

4. Устранение ошибки 1006 (Abnormal Closure) и настройка таймаутов

Классическая ситуация: рукопожатие успешно завершилось, сокет открылся, данные начали передаваться. Но если пользователь отвлекся и перестал отправлять сообщения, ровно через 60 секунд соединение самопроизвольно обрывается.

В консоли браузера при этом регистрируется событие WebSocket.onclose со следующим кодом:

// Вывод в консоли разработчика браузера (F12)
WebSocket connection to 'wss://app.example.com/ws/' failed: 
WebSocket is closed before the connection is established.
// CloseEvent: code = 1006 (Abnormal Closure), reason = ""

Код 1006 (Abnormal Closure) — это псевдокод браузера, зарезервированный спецификацией RFC 6455. Он указывает, что соединение было физически прервано на транспортном TCP-уровне (получен пакет TCP RST или TCP FIN) без предварительного обмена контрольным фреймом закрытия (Close Frame).

⚠️ Виновник сброса через 60 секунд: proxy_read_timeout

В Nginx параметр proxy_read_timeout по умолчанию составляет ровно 60s. Если между Nginx и вышестоящим приложением в течение 60 секунд не было передано ни одного байта, Nginx считает соединение зависшим и принудительно разрывает TCP-сокет.

Для устранения этой проблемы применяются два взаимодополняющих решения:

  1. Увеличение таймаута на стороне Nginx:
    Установите proxy_read_timeout 86400s; (24 часа) или 3600s (1 час) внутри блока location. Это предотвратит закрытие сокета самим веб-сервером.
  2. Внедрение Heartbeat (Ping/Pong фреймов) на стороне бэкенда:
    Даже если в Nginx таймаут равен суткам, сокет могут разорвать промежуточные узлы: NAT-роутеры домашних провайдеров, мобильные вышки операторов связи или облачные балансировщики. Протокол WebSocket имеет встроенные фреймы опроса: Ping (0x9) и Pong (0xA).

Пример реализации автоматического пинга в Node.js (библиотека ws):

// heartbeat.js на стороне Node.js сервера
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 3000 });

function heartbeat() {
  this.isAlive = true;
}

wss.on('connection', (ws) => {
  ws.isAlive = true;
  ws.on('pong', heartbeat);
});

// Отправляем Ping клиентам каждые 30 секунд
const interval = setInterval(() => {
  wss.clients.forEach((ws) => {
    if (ws.isAlive === false) return ws.terminate();
    ws.isAlive = false;
    ws.ping(); // Отправляет легковесный Ping-фрейм
  });
}, 30000);

wss.on('close', () => clearInterval(interval));

5. Безопасность WSS, SSL-сертификаты и специфика работы за Cloudflare

В современных веб-стандартах запуск WebSocket по открытому протоколу ws:// на страницах с HTTPS категорически заблокирован политикой смешанного содержимого (Mixed Content). Все браузеры при попытке установить открытое WebSocket-соединение с защищенного сайта выбросят ошибку безопасности:

Console (SecurityError)
SecurityError: Failed to construct 'WebSocket': An insecure WebSocket connection may not be initiated from a page loaded over HTTPS.

Поэтому на продакшене используется исключительно защищенный протокол wss:// (WebSocket Secure), представляющий собой сокет поверх TLS-туннеля. Проверить корректность настройки TLS и дату истечения сертификата на вашем домене всегда можно с помощью инструмента SSL-чекер SysKit.

Благодаря архитектуре Nginx нам не требуется реализовывать SSL/TLS внутри самого бэкенд-сервера (Node.js, Go или Python). Nginx выполняет функцию SSL Termination: терминирует входящее шифрованное WSS-соединение на 443 порту и проксирует расшифрованный трафик по быстрому локальному http://127.0.0.1:3000.

Для автоматического выпуска и продления бесплатного доверенного сертификата Let's Encrypt на VDS под управлением Ubuntu/Debian используется официальный Certbot:

bash
# Установка Certbot и плагина Nginx через snap
sudo snap install core && sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot

# Автоматический выпуск сертификата с настройкой редиректа на HTTPS
sudo certbot --nginx -d ws.example.com --redirect

Специфика проксирования WebSocket через Cloudflare

Если ваш VDS находится под защитой Cloudflare (включен оранжевый прокси-облако в DNS), необходимо учитывать важные особенности платформы:

  • Поддержка WebSocket по умолчанию: В бесплатных и платных тарифах Cloudflare проксирование WebSocket включено из коробки на стандартных HTTPS-портах (443, 2053, 2083, 2087, 2096, 8443).
  • Лимит неактивности (Idle Timeout): На всех тарифах Cloudflare жестко зафиксирован тайм-аут неактивного соединения, равный 100 секундам. Если в течение 100 секунд по сокету не прошло ни одного фрейма (данных или пинга), Cloudflare принудительно разрывает TCP-сессию.
  • Требование к Ping/Pong: Чтобы предотвратить обрыв сокетов через Cloudflare, интервал heartbeat-пингов в вашем бэкенде или фронтенде должен составлять не более 50–60 секунд.

6. Коды завершения WebSocket (RFC 6455): диагностика проблем

При закрытии WebSocket-соединения клиентское событие socket.onclose = (event) => { ... } возвращает числовой код (event.code) и текстовую причину (event.reason). Понимание этих кодов позволяет точно локализовать сбой — на стороне браузера, обратного прокси Nginx или бэкенда.

Код Статус RFC Причина сбоя Решение
1000 Normal Closure Штатное корректное закрытие сокета клиентом или сервером. Ошибки нет. Действий не требуется.
1001 Going Away Клиент закрыл вкладку браузера либо сервер перезагружается. Штатная обработка отключения подписчика.
1002 Protocol Error Нарушение протокола кадрирования RFC 6455 одной из сторон. Проверить совместимость WebSocket-библиотек бэкенда.
1006 Abnormal Closure TCP-сокет разорван без Close Frame (тайм-аут Nginx/NAT, сброс TCP RST). Настроить proxy_read_timeout 86400s и внедрить Ping/Pong пинги.
1008 Policy Violation Отказ сервера из-за неверной авторизации или Origin. Проверить JWT-токен или политику CORS/Origin на бэкенде.
1011 Internal Error Необработанное исключение или крах процесса на сервере приложений. Изучить логи приложения (PM2, Docker, systemd journalctl).
Онлайн-генератор безопасной конфигурации Nginx

Не пишите конфигурации вручную с риском допустить синтаксическую ошибку. Воспользуйтесь специализированным генератором Nginx Reverse Proxy SysKit, чтобы за секунды сгенерировать готовый production-блок со всеми директивами Upgrade, Connection и защитными заголовками.

Рекомендуемые VDS-провайдеры

Отказоустойчивые сервера с быстрыми NVMe и каналом до 1 Гбит/с

Выбор редакции
⚡

Timeweb Cloud — Быстрые NVMe VDS

Надежные облачные серверы с быстрыми NVMe-дисками, выделенным статическим IP и открытыми портами 80/443 для безотказной работы SSL и Nginx.

Конфигурация
1 vCPU 3.3 ГГц • 1 ГБ RAM • 15 ГБ NVMe • Трафик без лимита
Премиум Tier III
🔷

Selectel — Премиальные облачные серверы

Инфраструктура корпоративного уровня в дата-центрах Tier III: почасовая тарификация, приватные VLAN и прямое подключение к ведущим точкам обмена трафиком.

Конфигурация
1 vCPU • 2 ГБ RAM • 30 ГБ NVMe • Канал 100 Мбит/с
Для вебмастеров
🟠

Beget — Простота и стабильность VDS

Удобная интуитивная панель управления, автоматические ежедневные бэкапы и мгновенное развертывание LEMP-стека с предустановленным Nginx.

Конфигурация
1 vCPU 3.0 ГГц • 1 ГБ RAM • 20 ГБ NVMe • Трафик без лимита

Инструменты SysKit по теме статьи

Бесплатные утилиты для проверки и диагностики вашего сервера

Вопросы и ответы (FAQ)

Популярные вопросы вебмастеров и сисадминов по данной теме

Почему WebSocket разрывается ровно через 60 секунд после подключения?
Это стандартное значение директивы Nginx <code>proxy_read_timeout</code> (60 секунд). Если в течение этого времени ни клиент, ни бэкенд не отправляют данные через сокет, Nginx принудительно обрывает TCP-соединение со статусом 1006. Для решения увеличьте параметр до <code>86400s</code> (24 часа) и настройте отправку WebSocket Ping фреймов каждые 20–30 секунд.
Можно ли жестко прописать 'proxy_set_header Connection upgrade;' без блока map?
Так делать крайне не рекомендуется. Если директива задана статически, Nginx будет передавать заголовок <code>Connection: upgrade</code> абсолютно для всех входящих HTTP-запросов в этой секции, включая обычные GET/POST запросы. Это нарушает спецификацию HTTP/1.1 и приводит к ошибкам работы многих бэкенд-фреймворков. Конструкция с <code>map $http_upgrade $connection_upgrade</code> динамически передает <code>upgrade</code> только тогда, когда запрос содержит заголовок <code>Upgrade</code>, и <code>close</code> во всех остальных случаях.
Как проверить работу WebSocket на VDS из терминала без браузера?
Для быстрой консольной отладки WebSocket-соединений используется утилита <code>wscat</code> (устанавливается через <code>npm install -g wscat</code>). Выполните команду <code>wscat -c wss://example.com/ws</code>. Если подключение успешно установлено, вы увидите интерактивную строку ввода для отправки сообщений. Ошибки рукопожатия (400, 404, 502) отобразятся непосредственно в выводе утилиты.
Как правильно балансировать WebSocket-соединения между несколькими бэкендами в upstream?
WebSocket-сессии сохраняют постоянное состояние (stateful), поэтому для группы бэкендов в блоке <code>upstream</code> необходимо использовать директиву <code>ip_hash;</code> либо <code>hash $remote_addr consistent;</code>. Это гарантирует, что запросы повторного подключения клиента будут направляться на тот же самый инстанс бэкенда, сохраняющий сессионные данные пользователя.

Читайте также в блоге SysKit

Смежные руководства и полезные технические статьи