---
title: "Отладка: wscat, curl и логи соединений"
description: "Отладка websocket: рукопожатие глазами через curl, wscat -c в терминале, фреймы в DevTools и логи read error. Разбор ошибок 400 и 403 на Origin."
course: "WebSocket в Go: real-time чаты и уведомления"
url: "https://golangmaster.ru/courses/websocket/lessons/22-otladka-ws/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 16
---

# Отладка: wscat, curl и логи соединений

*Курс «WebSocket в Go: real-time чаты и уведомления», урок 22 из 25. ~16 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/websocket/lessons/22-otladka-ws/*

Отладка websocket начинается не с серверного кода, а с трёх инструментов: curl показывает рукопожатие как обычный HTTP-обмен, wscat разговаривает с сервером из терминала, а DevTools раскрывает каждый кадр по времени. Здесь разберём, что отвечают 400 и 403 при подключении, что скрывается за кодом 1006 и какие логи на сервере полезны, а какие — спам.

Клиент не подключается, экран молчит, консоль браузера пуста — и первое желание у большинства в этот момент перечитать код Upgrader'а. Зря: websocket-соединение — это сначала HTTP-запрос и HTTP-ответ, и вся диагностика отказа лежит в коде ответа и заголовках. Вопрос «кто виноват» раскладывается по трём этажам, и на каждом этаже свой инструмент. Не дошло рукопожатие — виноват HTTP-слой, и его показывает curl за одну команду. Рукопожатие прошло, а кадров нет — виноват протокол, и с ним разговаривает wscat из терминала. Кадры ходят, но клиент их теряет — смотрим фреймы в DevTools и логи read error на сервере. За вечер вы проходите эти три этажа сверху вниз и находите место поломки быстрее, чем открывается дамп трафика. Они же покрывают большинство обращений «не работает real-time»: остаток — сетевые политики инфраструктуры, и их лечат конфигом прокси, а не кодом сервера.

### Отладка websocket: рукопожатие глазами через curl

Рукопожатие — обычный GET с несколькими обязательными заголовками, поэтому его можно показать curl'ом и прочитать, как читают ответ любого API. Три заголовка обязательны: Connection: Upgrade, Upgrade: websocket и Sec-WebSocket-Version: 13, плюс Sec-WebSocket-Key — случайная строка, из которой сервер обязан вычислить Sec-WebSocket-Accept. Успех выглядит как HTTP/1.1 101 Switching Protocols: соединение переключено, дальше по нему идут кадры, и curl будет ждать их бесконечно — флаг -N отключает буферизацию, а Ctrl-C возвращает терминал. Самое ценное в этом упражнении — увидеть код отказа: 400 значит, что сервер не узнал websocket-запрос, 403 — что Upgrader его узнал, но не пустил. Обе цифры разобраны ниже, после таблицы.

### curl: полный рукопожатие в одну команду

```go
curl -i -N \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  http://localhost:8080/ws

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

Значение пары Key и Accept взято из примера RFC 6455: сервер вычисляет Accept как Base64 от SHA-1 ключа и константы GUID, и gorilla делает это по стандарту. Флаг -i показывает заголовки ответа, -N не буферизует вывод. Если ответ 101 — транспорт и Upgrader невиновны, ищите поломку выше по стеку, в кадрах.

### wscat: собеседник для сервера

curl подтверждает транспорт, но не умеет разговаривать протоколом. Для диалога ставится wscat — утилита на Node, которая открывает соединение и превращает терминал в чат: каждая введённая строка уходит кадром, каждый входящий кадр печатается в ответ. Установка — npm install -g wscat, запуск — wscat -c ws://localhost:8080/ws. Заголовки передаются флагом -H, и это способ проверить авторизацию из тринадцатого урока без браузера: -H Authorization: Bearer с настоящим токеном. wscat показывает и служебные события — Connected, Disconnected с кодом и причиной, — что делает его удобным для проверки heartbeat из шестого урока: открыли соединение, ничего не трогали и смотрим, жив ли ping-цикл. Инструменты разработчика подобного рода экономят минуты: сервер проверяется без фронтенда, сборки и открытий вкладок.

### Сессия wscat против сервера чата

```go
wscat -c ws://localhost:8080/ws -H "Authorization: Bearer eyJhbGciOi..."

Connected (press CTRL+C to quit)
> {"type":"join","room":"general"}
< {"type":"system","text":"u42 joined general"}
> {"type":"chat","room":"general","text":"привет"}
< {"type":"chat","user":"u42","text":"привет"}
```

Стрелки внутри сессии: строка с угловой скобкой — то, что ушло на сервер, с меньшей — то, что пришло с него. Вводим конверты из десятого урока как сырой JSON: сервер, который отвечает на join системным сообщением и на chat эхом, проверен без браузера. Ctrl-C закрывает соединение по close handshake — в логах сервера это чистый уход, а не обрыв.

### DevTools: вкладка Network и WS-фреймы

Браузерные инструменты разработчика дают то, чего нет у curl и wscat: хронологию кадров конкретной вкладки. Откройте Network, отфильтруйте по типу WS, кликните соединение — появится панель сообщений, где каждый кадр лежит строкой со временем, направлением и размером: исходящие и входящие подсвечены разными цветами, а служебные ping и close видны как отдельные записи. Здесь диагностируются вещи, которых сервер не видит: дубликаты соединений после багованного reconnect из семнадцатого урока (две строки вместо одной), пачки из двадцатого урока с массивом items внутри, отсутствие close frame при закрытии вкладки. Клик по кадру раскрывает JSON, и расследование «почему интерфейс не отрисовал сообщение» заканчивается за минуту: либо кадра нет — виноват сервер, либо кадр есть — виноват обработчик на клиенте. В Firefox панель называется Frames, механика та же: фильтр по WS, клик по соединению, список кадров с направлением.

### Молчащие соединения: где умирает heartbeat

Отдельный класс проблем — соединение, которое числится открытым, но ничего не передаёт: NAT дома и корпоративный брандмауэр убивают тихое TCP-соединение по таймауту простоя, обычно через несколько минут тишины. Симптом узнаваем: клиент работал час, ушёл в фон, вернулся к молчащему интерфейсу, а onclose пришёл через минуты после фактической смерти сокета. Диагностика двухсторонняя: DevTools показывает последние ping и их время, логи read error на сервере дают момент обрыва, и разрыв между отметками — то самое окно молчания. Лечение — heartbeat из шестого урока: ping раз в тридцать-шестьдесят секунд держит соединение помеченным как живое и переводит смерть из «неизвестно когда» в «не позже одного периода пинга». Проверяется вручную за две минуты: wscat, брошенный на Wi-Fi, и телефон на мобильном интернете дают два разных времени жизни, и оба становятся длиннее после включения пинга.

### Логи соединений: read error без спама

Серверный лог — последний этаж, и его главная задача — не показать всё, а отделить норму от поломки. В readPump любая ошибка чтения заканчивается возвратом, и наивный лог печатает каждую: на мобильной сети соединения рвутся по несколько раз в минуту, и лог превращается в поток abnormal closure 1006 — это норма, а не инцидент. Полезная строка содержит userID, код закрытия и причину, а уровень зависит от кода: 1000 и 1001 — штатные уходы, их логируют на уровне debug; 1006 — обрыв, info; неожиданные коды вроде 1009 при обычных сообщениях — warn, потому что намекают на лимит из четырнадцатого урока. Записывать тела сообщений в лог не стоит: там персональные данные, а для разбора протокола достаточно типа конверта.

> **Кейс: на проде 403, локально всё работало.** Деплой во вторую среду, и ни один клиент не подключается: curl изнутри кластера отвечает 403. Причина в CheckOrigin — Upgrader сравнивает Origin запроса со списком, а фронтенд теперь приходит с домена прода, которого в списке нет: локально разработчик гонял localhost, и всё было зелёным. Заодно ловится вторая ловушка: прокси и балансировщики иногда переписывают или вырезают Origin. Лечение — лог в ветке CheckOrigin: одна строка с отвергнутым Origin экономит час гаданий, а список доменов заводится в конфиг вместе с остальными адресами среды из пятнадцатого урока.

### Симптом, причина и инструмент

| Симптом | Типичная причина | Инструмент |
| --- | --- | --- |
| 400 Bad Request при подключении | запрос без заголовков Upgrade дошёл до Upgrader | curl -i: сравнить заголовки |
| 403 при подключении | CheckOrigin не пустил Origin | curl с -H Origin и лог в ветке |
| 101 получен, кадров нет | клиент не в комнате или не подписан | wscat: послать join и смотреть |
| onclose с кодом 1006 | обрыв без close handshake | логи read error на сервере |
| Кадры приходят, интерфейс молчит | обработчик не знает тип конверта | DevTools: раскрыть кадр и сверить type |
| Кадров в два раза больше, чем сообщений | дубль соединения после reconnect | DevTools: две строки в списке WS |

### Проверка: что означает 403

**Вопрос:** Upgrader ответил на рукопожатие кодом 403. Что это чаще всего значит?

- Сервер не понял websocket-запрос и ждёт обычный HTTP
- ✅ CheckOrigin вернул false: Origin запроса не в списке разрешённых
- Токен в query просрочен и соединение закрыто за лимит
- Кадр превысил SetReadLimit и сервер оборвал соединение

403 — вердикт ветки CheckOrigin: Upgrader узнал websocket-запрос, но Origin не входит в список разрешённых из пятнадцатого урока. 400 означает непонимание протокола, просроченный токен обычно даёт 401 на этапе авторизации до апгрейда, а превышение лимита рвёт уже открытое соединение кодом 1009.

### Проверка: загадочный 1006

**Вопрос:** Браузер вызывает onclose с кодом 1006. Что произошло?

- Сервер отправил close frame с кодом 1006
- ✅ Соединение умерло без close handshake: обрыв сети или крах процесса
- Клиент превысил лимит кадров и был отключён
- Ошибка в JSON одного из сообщений

1006 не ходит по сети: это внутренний код браузера для соединения, которое умерло, не попрощавшись, — Wi-Fi упал, процесс убит, брандмауэр закрыл сокет. На сервере симметричный симптом — ошибка чтения в readPump. Видеть 1006 у мобильных клиентов — норма; тревожиться стоит, когда он приходит всем одновременно.

### curl без заголовков Upgrade

```go
// сервер: обычный Upgrader из четвёртого урока
// var upgrader = websocket.Upgrader{}
// mux.HandleFunc("/ws", func(w, r) { upgrader.Upgrade(w, r, nil) })

curl -i http://localhost:8080/ws
```

**Что выведет программа?**

- 101 Switching Protocols: сервер сам добавит недостающие заголовки
- ✅ 400 Bad Request: запрос не выглядит как websocket-рукопожатие
- 403 Forbidden: Origin пуст, и CheckOrigin отказал
- 200 OK с HTML-страницей чата

Без Connection: Upgrade и Upgrade: websocket это обычный GET, и Upgrader отвечает 400 с текстом вида the client is not using the websocket protocol. Пустой Origin не служба: у Upgrader без строгого CheckOrigin он проходит, поэтому 403 здесь не при чём. Проверка заголовков — первый шаг любого расследования подключения.

> **Проверьте весь этаж на локальном Go.** Песочница курса не умеет сеть — соберите эхо-сервер из четвёртого урока на локальном Go и пройдите этажи сверху вниз: curl с заголовками покажет 101, wscat -c прогонит пару конвертов, вкладка WS в DevTools раскроет каждый кадр построчно. На весь круг уходит минут пять, и после него фраза «сервер не подключается» перестаёт быть паникой: вы точно знаете, на каком этаже искать.

### Соберите рукопожатие curl'ом

Впишите три пропуска: два обязательных заголовка и версию протокола.

```go
curl -i -N \
  -H "___: Upgrade" \
  -H "Upgrade: ___" \
  -H "Sec-WebSocket-Version: ___" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  http://localhost:8080/ws
```

Подсказка: Первый заголовок говорит о переключении протоколов, второй называет целевой протокол со строчной буквы, версия рукопожатия — 13.

Правильные ответы в пропусках: `Connection`, `websocket`, `13`

### Порядок расследования

Расставьте шаги: от кода ответа до логов сервера.

Правильный порядок строк:

1. `curl -i с заголовками Upgrade: прочитать код ответа`
2. `если 400: сверить заголовки рукопожатия`
3. `если 403: сверить Origin со списком CheckOrigin`
4. `если 101: открыть wscat и прогнать join`
5. `если кадры идут: раскрыть фреймы в DevTools`
6. `если и там пусто: читать логи read error сервера`

### Карточки: отладка

- **101 Switching Protocols** — Успешное рукопожатие: HTTP-соединение переключено на websocket. Всё, что ниже 101, — отказ до кадров.
- **Sec-WebSocket-Accept** — Ответ на Key: Base64 от SHA-1 ключа и GUID из RFC 6455. Сервер считает его, curl лишь показывает.
- **wscat -c** — Терминальный клиент: каждая строка — кадр, флаг -H несёт заголовки вроде Authorization. Сервер проверяется без браузера.
- **Код 1006** — Внутренний код браузера для обрыва без close handshake. Симметрия на сервере — ошибка чтения в readPump.
- **Уровни лога по кодам** — 1000 и 1001 — debug, 1006 — info, неожиданные вроде 1009 — warn. Тела сообщений в лог не пишем: там личные данные.

Три этажа отладки встали на место: curl отвечает за рукопожатие, wscat — за диалог, DevTools и логи — за кадры и их потери. Коды 400 и 403 больше не загадка, а указание на конкретную ветку. Когда соединение стабильно, следующий вопрос — что будет, когда клиентов станет больше одного сервера: [двадцать третий урок про масштабирование](/courses/websocket/lessons/23-masshtabirovanie/) строит мост между инстансами. Эхо-сервер для упражнений описан в [четвёртом уроке](/courses/websocket/lessons/04-pervyj-server/), ping-цикл и 1006 — в [шестом уроке про heartbeat](/courses/websocket/lessons/06-ping-pong/), лимит кадра и 1009 — в [четырнадцатом](/courses/websocket/lessons/14-limity/), а Origin и CheckOrigin — в [пятнадцатом уроке](/courses/websocket/lessons/15-origins/). Токен в заголовке для wscat берётся из [тринадцатого урока про авторизацию](/courses/websocket/lessons/13-avtorizaciya-ws/). Структурированные логи с уровнями удобно вести через [курс по zap](/courses/zap/) — в проде логов много, и без уровней они бесполезны. План курса — на странице [курса WebSocket](/courses/websocket/).

## Коротко: что запомнить

- Рукопожатие — обычный HTTP: curl с заголовками Upgrade показывает код ответа и Accept за одну команду.
- 101 означает, что транспорт жив: дальше ищите поломку в кадрах, а не в Upgrader'е.
- wscat -c проверяет протокол без браузера: конверты вводятся сырым JSON, заголовки летят через -H.
- DevTools на вкладке WS раскрывает каждый кадр по времени и направлению: дубликаты и пачки видны сразу.
- 400 — сервер не понял рукопожатие, 403 — CheckOrigin не пустил Origin, 1006 — обрыв без close handshake.
- Лог read error сортируется по кодам: штатные уходы в debug, обрывы в info, странные коды в warn.

## Домашнее задание

Поднимите эхо-сервер из четвёртого урока и пройдите три этажа: получите 101 через curl с заголовками Upgrade, прогоните конверты join и chat через wscat -c, найдите кадры на вкладке WS в DevTools. Затем ужесточите CheckOrigin под конкретный домен и добейтесь 403 с чужим Origin, добавив в ветку лог с отвергнутым значением. В readPump заведите строку лога с userID и кодом закрытия и убедитесь, что Ctrl-C в wscat даёт чистый 1000, а обрыв Wi-Fi — 1006.

## Вопросы и ответы по уроку

### Можно ли обойтись без wscat, если есть браузер?

Можно, но дороже: вкладка требует собранного фронтенда и правильного Origin, а wscat работает из любой папки и с любым заголовком. В CI и на сервере без графики браузера нет вовсе, и терминальный клиент остаётся единственным способом поговорить с сокетом. Для скриптов есть ещё вариант на Go из двадцать первого урока — Dialer в main-функции.

### Почему нельзя логировать тела сообщений?

В телах лежат персональные данные: тексты переписки, имена, иногда токены, случайно присланные клиентом. Логи живут дольше запросов, копируются в агрегаторы и читаются всеми, у кого есть доступ к инфраструктуре, поэтому тело в логе — утечка по определению. Для отладки протокола хватает типа конверта, userID и размера кадра.

### Как поймать рукопожатие между клиентом и прокси?

curl до и после прокси: сравните заголовки запроса и код ответа на обоих участках. Если напрямую 101, а через прокси 400 или обрыв — прокси вырезает заголовки Upgrade или держит соединение открытым для буферизации, и это настраивается на его стороне. Помните, что некоторые прокси по умолчанию не пропускают websocket и требуют явного включения.
