ERR_HTTP2_PROTOCOL_ERROR в браузере и сообщения вида http2 framing layer в клиентах (curl, Go, Node, gRPC) — одна из самых неприятных категорий инцидентов: ошибка «плавающая», логов мало, а внешне всё похоже на случайные обрывы.
Почти всегда причина в одном из трёх слоёв:
TLS/ALPN: клиент ожидает
h2, а сервер/прокси согласовал другой протокол или отдал не тот трафик по «h2»-каналу.Проксирование: промежуточный узел режет/буферизует ответ или неверно проксирует gRPC, и клиент видит протокольный сбой.
Лимиты и время жизни соединений: keep-alive, таймауты и лимиты запросов закрывают соединение в «неудачный» момент, что в HTTP/2 часто выглядит как протокольная ошибка.
Ниже — пошаговый чек-лист, который обычно приводит к первопричине без гадания.
Как проявляется проблема: отличаем HTTP/2 от «просто 502»
Типовые симптомы:
Chrome/Edge: ERR_HTTP2_PROTOCOL_ERROR на конкретном эндпоинте API или при нагрузке.
curl:
HTTP/2 stream 0 was not closed cleanly,PROTOCOL_ERROR, иногдаreceived GOAWAY«внезапно».Go/Node:
http2: server sent GOAWAY and closed the connectionили «http2 framing layer».gRPC:
transport: error while dialing,RST_STREAM,INTERNAL: received RST_STREAM.
Ключевой момент: клиент жалуется именно на протокол, а не на HTTP-код. Это почти всегда означает несоответствие ожиданий по HTTP/2 на одном из участков цепочки.
Быстрая проверка: действительно ли до сервера доезжает HTTP/2
Сначала нужно понять, где именно «ломается» HTTP/2: на внешнем входе, на балансере, на Nginx, на апстриме.
1) Проверяем ALPN и выбранный протокол
HTTP/2 поверх TLS включается через ALPN (Application-Layer Protocol Negotiation). Если ALPN не согласован, клиент уйдёт в HTTP/1.1 или оборвёт соединение, если ожидал h2 (особенно для gRPC).
curl -vk --http2 https://api.example.com/health
В выводе ищите строки про ALPN и протокол. Если согласован h2 — хорошо. Если согласован только http/1.1, а вы уверены, что «включали HTTP/2», разбираемся с конфигом и фронтовыми прокси.
2) Если есть промежуточный L4 прокси: вспоминаем про ssl_preread
Частая схема: перед «основным» Nginx стоит stream-балансер (L4), который маршрутизирует по SNI, а дальше уже TLS терминируется на бекенде. В такой архитектуре используют ssl_preread в stream-контексте.
ssl_prereadсам по себе не «включает HTTP/2». Он читает ClientHello (SNI/ALPN) для маршрутизации. Ошибки начинаются, когда L4 отправляет трафик на неправильный бекенд или на бекенд без нужного ALPN/сертификата.
Проверьте, что поток с SNI api.example.com стабильно попадает туда, где в server для 443 действительно настроен http2 и корректная цепочка сертификата.
3) Минимальная телеметрия: что думает сам Nginx
Убедитесь, что в access-логах вы видите протокол. В Nginx можно логировать $server_protocol (для HTTP/2 будет HTTP/2.0), а также времена ответа апстрима.
Пример формата (как ориентир):
log_format main_ext '$remote_addr host=$host req="$request" '
'status=$status bytes=$body_bytes_sent '
'proto=$server_protocol rt=$request_time '
'urt=$upstream_response_time ua="$http_user_agent"';
Если в момент ошибки в логах видите proto=HTTP/1.1, а клиенты жалуются на HTTP/2 — это красный флаг: где-то до Nginx происходит несогласование протокола или неправильная маршрутизация.

Самые частые причины ERR_HTTP2_PROTOCOL_ERROR в связке Nginx + API
Причина 1: HTTP/2 включён не там (или включён «не тот h2»)
Для HTTPS-виртуалхоста Nginx HTTP/2 включается на listen 443 ssl http2;. Если у вас несколько server-блоков, легко «промахнуться»: один блок обслуживает редирект или дефолтный vhost, другой — API. В итоге часть запросов попадает в vhost без HTTP/2 или с неправильным сертификатом.
Что проверить:
Порядок
server-блоков иserver_name(нет ли «дефолтного» блока, который перехватывает SNI).Нет ли конфликта на уровне stream-прокси (SNI уходит не туда).
Сертификат и цепочка (при проблемах рукопожатия и ретраях клиенты иногда «маскируют» первичную ошибку под протокольную).
Причина 2: gRPC проксируется как обычный HTTP
Если ваш API частично или полностью gRPC, проксирование через proxy_pass часто приводит к ошибкам уровня http2 framing layer. gRPC требует корректного HTTP/2 и правильного проксирования через grpc_pass (а также корректных таймаутов).
Признаки:
REST-эндпоинты работают, а gRPC-метод падает.
Падает только на больших сообщениях или стриминге.
В логах апстрим «как будто ответил», но клиент всё равно видит протокольную ошибку.
Минимальная рабочая схема для gRPC в Nginx (как ориентир):
server {
listen 443 ssl http2;
server_name api.example.com;
location /mypackage.MyService/ {
grpc_pass grpc://127.0.0.1:50051;
grpc_read_timeout 300s;
grpc_send_timeout 300s;
}
}
Если апстрим говорит h2c (HTTP/2 cleartext) — используйте grpc://. Если TLS до апстрима — grpcs:// и отдельные настройки доверия/проверки сертификата.
Если у вас смешанный стек (gRPC-Web, Envoy и т.п.), полезно свериться с архитектурой проксирования: как проксировать gRPC-Web через Envoy.
Причина 3: proxy_buffering и «неожиданная» буферизация для API/стриминга
Директива proxy_buffering чаще всплывает при SSE/WebSocket и больших ответах. Но на HTTP/2 она тоже влияет: Nginx может буферизовать ответ от апстрима и отдавать клиенту «рывками», а при таймаутах/обрывах это превращается в протокольные симптомы.
Где чаще всего больно:
SSE (Server-Sent Events) и длинные ответы.
Большие JSON, которые генерируются долго.
Апстрим отдаёт поток (chunked/stream), а фронт пытается его «собрать».
Практический подход: для стриминговых эндпоинтов пробуйте отключать буферизацию и поднимать таймаут чтения:
location /events/ {
proxy_pass http://backend;
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
}
Для «обычного» API отключать proxy_buffering повсеместно не нужно: это может ухудшить производительность. Но точечно — очень часто лечит нестабильные ERR_HTTP2_PROTOCOL_ERROR на длинных ответах.
Причина 4: keepalive_requests и разрывы соединения «между запросами»
HTTP/2 держит одно TCP/TLS-соединение и мультиплексирует запросы. Если вы агрессивно ограничиваете жизнь соединения, клиент может получить закрытие в момент, когда планировал открыть новый stream. В браузере это иногда выглядит как ERR_HTTP2_PROTOCOL_ERROR, особенно при параллельных запросах.
Проверьте:
keepalive_requests(сколько запросов разрешено на одно соединение).keepalive_timeout(сколько живёт keep-alive).не прилетает ли клиенту
GOAWAYслишком рано.
Если соединения часто закрываются по лимиту, увеличьте keepalive_requests для API, где много мелких запросов. Это снижает churn соединений и уменьшает вероятность гонок закрытия.
Причина 5: апстрим «сыпется» под паттерн HTTP/2 (особенно через прокси)
Даже если наружу Nginx отдаёт HTTP/2, внутренняя часть часто остаётся HTTP/1.1 до апстрима. Тогда реальная проблема может быть в апстриме: он не выдерживает burst запросов, уходит в таймауты, закрывает соединения или отдаёт некорректные заголовки.
Что сделать прагматично:
Сравнить поведение при
curl --http1.1иcurl --http2на одном и том же эндпоинте.Проверить корреляцию с RPS, размером ответов, временем генерации.
Посмотреть error-лог Nginx на «upstream prematurely closed connection» и таймауты.
Пошаговый сценарий диагностики (рабочий чек-лист)
Шаг 1. Воспроизводим с curl и фиксируем протокол
curl -svk --http2 https://api.example.com/v1/ping
curl -svk --http1.1 https://api.example.com/v1/ping
Если HTTP/1.1 стабилен, а HTTP/2 нет — проблема почти точно в h2-цепочке (ALPN, прокси, gRPC, лимиты, закрытие соединения).
Шаг 2. Ищем GOAWAY/RST в поведении клиента
В мире HTTP/2 «красные кнопки» — это GOAWAY (сервер уходит) и RST_STREAM (сбрасывается поток). Браузер часто не показывает детали, но в curl/клиентских логах их видно.
Если это gRPC — смотрите коды статуса и сообщения transport-слоя: они часто точнее указывают, на каком участке обрыв.
Шаг 3. Временно упрощаем цепочку: прямой доступ к Nginx
Если перед Nginx стоит CDN/балансер/ingress — попробуйте на время (в тестовой зоне или по отдельному хостнейму) дать прямой доступ к Nginx. Цель — понять, генерирует ли проблему сам Nginx/апстрим, или это артефакт внешнего прокси.
Шаг 4. Проверяем конфигурацию server-блоков на 443
Минимальный набор вопросов:
Есть ли ровно тот
server, который матчится по SNI на проблемный домен?На нужном ли
listenвключёнhttp2?Нет ли редиректов/перехвата в другом vhost?
Полезно прогнать синтаксическую проверку и распечатать полный конфиг:
nginx -t
nginx -T | sed -n '1,200p'
Шаг 5. Если есть stream + ssl_preread: валидируем маршрутизацию
При L4-маршрутизации «протокольная» ошибка может означать просто «попали не туда». Например, SNI уехал на бекенд, где ожидают другой протокол (или plain HTTP), и клиент получает мусор вместо HTTP/2-фреймов.
На время диагностики удобно выделить отдельный порт/бекенд и убедиться, что SNI точно идёт куда нужно. Если параллельно вы экспериментируете с терминацией HTTP/3/QUIC на edge, сравните подходы: терминация HTTP/3/QUIC на балансере.

Набор «безопасных» правок, которые часто стабилизируют HTTP/2 для API
Это не универсальная «таблетка», но набор действий, который часто приводит конфиг в предсказуемое состояние:
Явно включить HTTP/2 на нужном 443-vhost:
listen 443 ssl http2;Для gRPC использовать
grpc_pass, а неproxy_pass.Точечно отключить
proxy_bufferingдля стриминговых эндпоинтов и увеличить таймауты чтения.Проверить и при необходимости увеличить
keepalive_requestsиkeepalive_timeout.Убедиться, что TLS согласует ALPN (
h2) и нет конфликтов SNI/сертификатов.
Если проблема всплыла после перевыпуска сертификата или изменения цепочки, проверьте корректность установки и совместимость. Для продакшн-сервисов обычно удобнее держать управляемую историю сертификатов и обновления заранее — в том числе через SSL-сертификаты с понятным циклом продления.
Почему ошибка называется «http2 framing layer» и при чём тут Nginx
HTTP/2 — бинарный протокол, данные идут «фреймами». Сообщение «http2 framing layer» обычно означает, что клиент получил последовательность байт, которая не соответствует формату фреймов HTTP/2, либо получил корректные фреймы, но в неожиданном порядке.
В контексте Nginx чаще всего это происходит, когда:
на входе договорились о
h2, но далее в цепочке внезапно отдали HTTP/1.1 (неверный vhost, дефолтныйserver, ошибка SNI-маршрутизации);gRPC пошёл через неправильный
locationили черезproxy_pass;соединение оборвали в момент активных потоков (лимиты/keepalive/таймауты), а клиент интерпретировал это как протокольный сбой.
Мини-памятка: что собирать для разбора инцидента
Чтобы быстро локализовать проблему (и не возвращаться к ней через месяц), соберите в один пакет:
воспроизведение
curl -vk --http2(полный вывод);время инцидента и корреляцию с RPS/нагрузкой;
access/error логи Nginx за интервал (с
$server_protocolи upstream timings);конкретный
server-блок для домена иlocationпроблемного эндпоинта;если используется stream +
ssl_preread— конфиг stream и список бекендов, куда может уйти трафик.
Как только у вас появляется связка «curl-вывод +
$server_protocolв логах + точный vhost», ERR_HTTP2_PROTOCOL_ERROR перестаёт быть мистикой и превращается в обычную задачу конфигурации.
Вывод
ERR_HTTP2_PROTOCOL_ERROR и «http2 framing layer» почти всегда указывают не на «плохой интернет», а на несогласованность протокола и ожиданий между клиентом и вашей цепочкой прокси/апстримов. Начинайте с ALPN и маршрутизации по SNI, отдельно проверьте gRPC, затем разберите буферизацию (proxy_buffering) и ограничения жизни соединений (keepalive_requests и таймауты). Этот порядок обычно даёт результат быстрее всего.


