ERR_QUIC_PROTOCOL_ERROR и ERR_HTTP2_PROTOCOL_ERROR: что такое HTTP/2, HTTP/3 и QUIC и почему они ломаются
Сайт открывается в Firefox и не открывается в Chrome, curl отдаёт 200, а браузер пишет «Сайт не позволяет установить соединение». За двумя кодами стоят два протокола: HTTP/2 поверх TCP и HTTP/3 поверх QUIC и UDP. Разбираем, как браузер выбирает протокол, что ломается на сервере, в сети и у пользователя, как локализовать проблему одной командой curl и как безопасно включать и выключать HTTP/3.
ERR_HTTP2_PROTOCOL_ERROR (код -337 в Chromium) и ERR_QUIC_PROTOCOL_ERROR (код -356) — ошибки, при которых TLS-соединение с сайтом установлено, но обмен по согласованному протоколу нарушил спецификацию: HTTP/2 поверх TCP в первом случае, HTTP/3 поверх QUIC и UDP во втором. Это не «сайт недоступен» и не ошибка сертификата: сервер найден, шифрование поднято, а дальше одна из сторон прислала кадр, который другая не смогла принять. Источник — приложение с некорректными заголовками, балансировщик, антивирус или файрвол, пропускающий TCP 443 и режущий UDP 443.
Чем HTTP/1.1, HTTP/2 и HTTP/3 отличаются друг от друга
HTTP/2 сохранил семантику HTTP/1.1 (методы, коды, заголовки), но заменил текст бинарными кадрами и гоняет десятки запросов в одном TCP-соединении. HTTP/3 перенёс те же кадры с TCP на QUIC — транспорт поверх UDP со встроенным TLS 1.3, где потеря пакета тормозит один поток, а не всё соединение.
| Признак | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| RFC | 2068 (1997), 2616 (1999), сейчас 9112 (2022) | 7540 (май 2015), сейчас 9113 (июнь 2022) | 9114 (июнь 2022); QUIC — 9000 (май 2021) |
| Транспорт | TCP, текст | TCP, бинарные кадры | QUIC поверх UDP 443 |
| Мультиплексирование | Нет: один запрос за раз, до 6 соединений на хост | Потоки в одном соединении (nginx: 128 по умолчанию) | Независимые потоки QUIC |
| Head-of-line blocking | На уровне HTTP: очередь на соединение | На уровне TCP: потерянный сегмент останавливает все потоки | Нет: потеря тормозит только свой поток |
| Шифрование | Необязательно | Браузеры — только h2 поверх TLS 1.2+ | Всегда: TLS 1.3 встроен в QUIC (RFC 9001) |
| nginx | Всегда | С 1.9.5 (2015); http2 on с 1.25.1 | С 1.25.0 (май 2023), сборка с --with-http_v3_module |
| Apache httpd | Всегда | mod_http2 с 2.4.17; с 2.4.27 не согласуется при MPM prefork | Штатного модуля нет |
| Cloudflare | Да | Да, плюс «HTTP/2 to Origin» | Переключатель на всех планах; до origin HTTP/3 не ходит |
| Браузеры по умолчанию | Все | Chrome 41, Firefox 36 (2015) | Chrome и Edge 87, Firefox 88, Safari 16 (по Can I use) |
По данным W3Techs на 23 сентября 2026 года, HTTP/3 используют 40,8 % сайтов, HTTP/2 — 34,8 %: ошибки новых протоколов стали такими же обыденными, как ERR_CONNECTION_RESET. Разница между TCP и UDP, на которой держится различие HTTP/2 и HTTP/3, — в статье «TCP и UDP: в чём разница».
Как браузер выбирает между HTTP/1.1, HTTP/2 и HTTP/3
HTTP/2 согласуется внутри TLS-рукопожатия через расширение ALPN: клиент предлагает «h2, http/1.1», сервер выбирает один. Про HTTP/3 браузер при первом визите не знает ничего — он приходит по TCP, получает в ответе заголовок Alt-Svc с токеном h3 (RFC 7838), запоминает его и только следующие соединения пробует открыть по QUIC.
| Шаг | Что происходит | Если не получилось |
|---|---|---|
| 1. Первый визит: TCP + TLS, ALPN | Сервер ответил «h2» — кадры HTTP/2; «http/1.1» или молчание — текстовый HTTP/1.1 | Выбрал h2, а прислал текст HTTP/1.1 — ERR_HTTP2_PROTOCOL_ERROR; h2 на шифре из чёрного списка RFC 9113 — ERR_HTTP2_INADEQUATE_TRANSPORT_SECURITY (-360) |
| 2. В ответе Alt-Svc: h3=":443"; ma=86400 | Браузер запоминает: хост принимает HTTP/3 на UDP 443, срок — сутки | Нет заголовка — сайт живёт на HTTP/2, даже если QUIC на сервере включён |
| 3. Следующее соединение: гонка | Chromium открывает QUIC первым, TCP откладывает на время RTT, но не больше 3 с (http_stream_factory_job_controller.cc) | QUIC-рукопожатие не прошло — побеждает TCP, пользователь видит задержку |
| 4. Пометка «broken» | По broken_alternative_services.cc: после сбоя 300 с без попыток QUIC, каждый следующий сбой удваивает паузу, потолок около двух суток | После починки UDP 443 часть пользователей ещё часами сидит на HTTP/2 |
| 5. Обрыв QUIC посреди запроса | Заголовки ответа ещё не пришли — Chromium молча повторяет запрос по TCP (http_network_transaction.cc) | Заголовки уже получены — повторить нельзя, пользователь видит ERR_QUIC_PROTOCOL_ERROR |
Два вывода. ERR_QUIC_PROTOCOL_ERROR почти не встречается на первом заходе — сначала браузеру нужен Alt-Svc. И если QUIC недоступен полностью, Chrome сам откатится на TCP; ошибка появляется там, где UDP проходит частично: рукопожатие удаётся, а потом пакеты теряются или соединение сбрасывает промежуточный узел. Firefox и Safari узнают про h3 ещё из DNS-записи HTTPS (RFC 9460); в Chrome эта возможность, по обсуждениям в chromium-dev, оставалась за флагом.
Как за минуту понять, какой протокол ломается
Три запроса curl с принудительной версией отвечают на главный вопрос: сбоит HTTP/2, HTTP/3 или сайт целиком. Для HTTP/3 нужна сборка curl со словом HTTP3 в выводе curl --version.
$ curl -sI --http1.1 https://example.com/ | head -1 HTTP/1.1 200 OK $ curl -sI --http2 https://example.com/ | head -1 HTTP/2 200 # --http3 при неудаче тихо откатывается на h2/h1; # для диагностики нужен строгий режим $ curl -sI --http3-only https://example.com/ | head -1 HTTP/3 200 # пример вывода, когда UDP 443 не отвечает $ curl -sI --http3-only https://example.com/ curl: (96) QUIC connection has been shut down # пример вывода: HTTP/2 ломается на конкретном URL $ curl -sv --http2 -o /dev/null https://example.com/export.php < HTTP/2 200 < content-length: 48211 < content-encoding: gzip * HTTP/2 stream 1 was not closed cleanly: PROTOCOL_ERROR (err 1) curl: (92) HTTP/2 stream 1 was not closed cleanly: PROTOCOL_ERROR
Коды выхода curl по libcurl-errors: 16 — сбой на уровне кадров HTTP/2, 92 — ошибка в одном потоке HTTP/2, 95 — ошибка уровня HTTP/3, 96 — QUIC-соединение не установлено. Падает только --http3-only — проблема в UDP-пути; падает --http2 при рабочем --http1.1 — в кадрах HTTP/2 на сервере или прокси. В последнем примере статус 200 пришёл, а поток закрыт с ошибкой — в DevTools это выглядит как «net::ERR_HTTP2_PROTOCOL_ERROR 200 (OK)». Второй инструмент — Alt-Svc: обещает ли сервер HTTP/3 и на каком порту.
$ curl -sI https://example.com/ | grep -i '^alt-svc' # пример вывода: nginx с http3 on или Cloudflare alt-svc: h3=":443"; ma=86400 # пример вывода: черновик h3-29 — современный Chrome его игнорирует alt-svc: h3-29=":443"; ma=2592000
Третий — сам браузер. Отключите QUIC и повторите: ошибка исчезла — сломан UDP-путь. Это диагноз, а не лечение: флаг живёт на одном компьютере, у посетителей ничего не изменится.
chrome://flags/#enable-quic # Experimental QUIC protocol → Disabled,
edge://flags/#enable-quic # затем перезапуск браузера
about:config # Firefox: network.http.http3.enable = false
chrome://net-export # лог сетевого стека с уточнением ошибки,
# например QUIC_NETWORK_IDLE_TIMEOUT
Откуда берётся ERR_HTTP2_PROTOCOL_ERROR
Чаще всего виноват ответ сервера, нарушающий раздел 8.2.1 RFC 9113: имена заголовков только в нижнем регистре, значения без пробелов по краям и без CR, LF, NUL, а Connection, Keep-Alive, Transfer-Encoding и Upgrade запрещены. Вторая причина — тело, длина которого не совпадает с Content-Length: по разделу 8.1.1 такой ответ malformed, и Chrome закрывает поток с PROTOCOL_ERROR, хотя статус 200 уже показал. Реже виноват объём: десятки Set-Cookie превышают лимит SETTINGS_MAX_HEADER_LIST_SIZE клиента, и Chrome отвечает ERR_RESPONSE_HEADERS_TOO_BIG (-325).
| Где | Причина | Как проверить | Как исправить |
|---|---|---|---|
| Приложение | Заголовок с переносом строки, пробелом в конце или управляющим символом (Set-Cookie, Content-Disposition из пользовательских данных) | curl -sD - --http1.1 показывает сырые заголовки; ошибка на конкретных URL | Валидировать значения; имена файлов кодировать по RFC 8187 (filename*=UTF-8'') |
| Сжатие | Приложение выставило Content-Length несжатого тела, а mod_deflate или gzip в прокси его сжал; вывод после header() в PHP | curl -v --http2: «not closed cleanly: PROTOCOL_ERROR»; по --http1.1 size_download меньше Content-Length | Сжимать на одном уровне; не задавать Content-Length вручную |
| Apache | mod_http2 с MPM prefork и mod_php: до 2.4.27 зависшие потоки и обрывы, с 2.4.27 HTTP/2 в prefork не согласуется | apachectl -V | grep MPM | MPM event и PHP через php-fpm (mod_proxy_fcgi) |
| Балансировщик | L4-балансировщик пропускает ALPN h2 до бэкенда, знающего только HTTP/1.1; прокси пробрасывает Transfer-Encoding: chunked в HTTP/2 | curl --http2 --resolve напрямую в бэкенд работает, через балансировщик — нет | Терминировать TLS там, где есть HTTP/2, или убрать h2 из ALPN |
| Клиент | Антивирус или корпоративный прокси с инспекцией HTTPS переупаковывает кадры с ошибками | Ошибка на многих сайтах у одного человека; сертификат выдан антивирусом | Отключить сканирование HTTPS, сайт в исключения |
Откуда берётся ERR_QUIC_PROTOCOL_ERROR
QUIC ломается не форматом заголовков, а транспортом: UDP 443 закрыт файрволом, провайдер или DPI режет или троттлит UDP, MTU на пути меньше требуемых 1200 байт. Общее у всех сбоев одно: по TCP тот же сайт открывается идеально, и администратор со своей машины проблемы не видит.
| Причина | Признак | Как проверить | Как исправить |
|---|---|---|---|
| UDP 443 закрыт на сервере или в security group | Alt-Svc есть, --http3-only падает с 96; жалобы на медленный первый заход (до 3 с гонки) | ss -ulnp на сервере; tcpdump 'udp port 443' во время запроса снаружи | Открыть UDP 443 в обе стороны; пока не открыли — убрать Alt-Svc |
| UDP проходит частично: DPI, провайдер, корпоративный файрвол | Рукопожатие удаётся, затем QUIC_NETWORK_IDLE_TIMEOUT или QUIC_TOO_MANY_RTOS; страдают все сайты с h3 у клиентов одного оператора | С клиента --http3-only на несколько чужих сайтов; сменить канал на мобильный | На сервере — держать исправным TCP-путь; на клиенте — отключить QUIC |
| MTU меньше 1228 (IPv4) или 1248 (IPv6) | RFC 9000, раздел 14: путь обязан пропускать датаграммы 1200 байт; туннель с MTU 1200 QUIC не пропустит | ping -M do -s 1172 host (Linux), ping -f -l 1172 (Windows): 1172 + 28 = 1200 | MTU туннеля не ниже 1280 |
| Неверный Alt-Svc | Порт, куда UDP не проброшен (h3=":8443" за NAT), h3 на балансировщике без QUIC или только черновики h3-29 без RFC-версии; кэшируется на ma секунд | curl -sI | grep alt-svc и --http3-only на указанный порт | Отдавать токен h3 только там, где UDP доступен снаружи; на время экспериментов ma=300 |
| Клиент: антивирус, файрвол, VPN | UDP 443 заблокирован приложению; ошибка на всех сайтах с HTTP/3 у одного человека | Отключить QUIC во флагах — ошибка исчезла; с другого устройства всё работает | Правило для UDP 443, исключение в антивирусе, обновить VPN |
Совет «проверьте UDP-порт через nc -zvu host 443» не работает: у UDP нет рукопожатия, и nc считает порт открытым, если не прилетело ICMP port unreachable. QUIC-сервер на мусор не отвечает, файрвол с DROP — тоже, так что «succeeded» будет и для рабочего сервера, и для закрытого порта. Проверяйте настоящим QUIC-клиентом или дампом на сервере:
# Слушает ли nginx UDP 443
$ ss -ulnp | grep ':443 '
# пример вывода
UNCONN 0 0 0.0.0.0:443 0.0.0.0:* users:(("nginx",pid=812,fd=11))
# Доходят ли QUIC Initial снаружи: запустите curl --http3-only
# с другой машины и смотрите на сервере
$ sudo tcpdump -ni eth0 'udp port 443' -c 4
# пример вывода: запрос пришёл, ответ ушёл — путь исправен
12:40:01.104 IP 198.51.100.7.54123 > 203.0.113.10.443: UDP, length 1200
12:40:01.106 IP 203.0.113.10.443 > 198.51.100.7.54123: UDP, length 1200
# Ничего не пришло — файрвол или security group
$ sudo ufw allow 443/udp
Как правильно включить HTTP/2 и HTTP/3 в nginx и Cloudflare
С nginx 1.25.1 HTTP/2 включается директивой http2 on, параметр listen ... http2 устарел. HTTP/3 требует сборки с --with-http_v3_module, второго listen с параметром quic на том же порту и заголовка Alt-Svc, который nginx сам не добавляет.
server {
listen 443 ssl;
listen 443 quic reuseport; # reuseport — в одном server на порт
server_name example.com;
http2 on;
http3 on;
ssl_certificate /etc/ssl/example.com/fullchain.pem;
ssl_certificate_key /etc/ssl/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3; # QUIC работает только с 1.3
# 0-RTT для QUIC — с nginx 1.29.1 и OpenSSL 3.5.1+
# ssl_early_data on;
# add_header в location ПЕРЕКРЫВАЕТ add_header уровня server —
# проверьте, что Alt-Svc отдаётся на всех типах URL
add_header Alt-Svc 'h3=":443"; ma=86400' always;
location / { proxy_pass http://127.0.0.1:8080; }
}
После nginx -t и reload прогоните три команды curl: --http2 должен вернуть «HTTP/2 200», --http3-only — «HTTP/3 200», grep по alt-svc — заголовок на каждом типе URL, включая статику из отдельных location. Отдельный сертификат для QUIC не нужен: TLS 1.3 внутри него использует те же файлы; само рукопожатие разобрано в статье «Как работает HTTPS».
В Cloudflare HTTP/3 (with QUIC) включается переключателем в Speed → Settings → Protocol Optimization на всех планах, но до origin Cloudflare ходит только по HTTP/1.1 или HTTP/2. Поэтому ERR_QUIC_PROTOCOL_ERROR у посетителя сайта за Cloudflare рождается между ним и краем сети Cloudflare, а ERR_HTTP2_PROTOCOL_ERROR может прийти и с origin — некорректные заголовки бэкенда Cloudflare транслирует дальше.
Сначала уберите Alt-Svc и дождитесь истечения ma (при 86400 — сутки), потом снимайте listen quic. Иначе браузеры с закэшированным Alt-Svc ещё сутки будут пробовать QUIC: Chrome откатится на TCP и пометит хост broken на 5 минут, но пользователи заметят задержку, а те, у кого UDP проходит частично, — ошибку.
Симптом → протокол → причина → что делать
| Симптом | Протокол | Вероятная причина | Что делать |
|---|---|---|---|
| Ошибка на одном URL (выгрузка, скачивание), остальное работает | HTTP/2 | Content-Length не совпадает с телом или невалидный заголовок | curl -v --http2 на этот URL; править приложение или сжатие |
| Ошибка у одного пользователя на многих сайтах | Любой | Антивирус с инспекцией HTTPS, VPN, файрвол на UDP 443 | Отключить защиту HTTPS, выключить QUIC во флагах |
| Первое открытие медленное (2–3 с), потом быстро | QUIC | Alt-Svc обещает h3, UDP 443 закрыт, Chrome ждёт до 3 с и уходит на TCP | Открыть UDP 443 или убрать Alt-Svc |
| Ошибка у клиентов одного оператора или офиса | QUIC | DPI, корпоративный файрвол, MTU туннеля | Сравнить из нескольких сетей; ping -M do -s 1172; сервер не трогать |
Как узнать о протокольной ошибке раньше пользователей
Обе ошибки избирательны: ломается конкретный URL, протокол или сеть, а «сайт в целом» отвечает 200. Нужны регулярные запросы к проблемным URL из нескольких сетей с историей по каждой точке.
Пруфен выполняет HTTP-проверки с интервалом от 2 минут из 20 точек мониторинга — 17 в России, а также Минск, Астана и Испания. Мониторинг фиксирует, что страница перестала отдаваться с ожидаемым кодом, а сравнение по 20 точкам показывает, у всех ли: падает из всех локаций — ищите на сервере заголовки и Content-Length; из части — смотрите на сеть между этими регионами и сервером. В настройках HTTP-проверки задаются метод, заголовки и ожидаемый код ответа, чтобы проверять именно проблемный URL.
Пруфен присылает уведомление в Email, Telegram или Макс, как только проверка перестала проходить, и второе — когда страница снова отдаётся. Если ресурс так и не поднялся, подключается голосовой звонок. Время обнаружения равно интервалу проверки. Бесплатный план включает 5 проверок — хватит на главную, API и страницу экспорта, которая ломается после каждого обновления.
Частые вопросы
Нужно ли вообще включать HTTP/3 на своём сайте?
За Cloudflare или другим CDN — да, одним переключателем. На собственном nginx включайте, когда готовы держать открытым UDP 443 и следить за ним так же, как за TCP: выигрыш заметен на мобильных сетях, где потери пакетов останавливают HTTP/2 целиком. Если UDP открыть нельзя, не отдавайте Alt-Svc — обещанный, но недоступный h3 хуже честного HTTP/2.
Почему ошибка только в Chrome, а в Firefox сайт открывается?
У браузеров разный кэш Alt-Svc и разная логика отката: Firefox мог уже пометить HTTP/3 для хоста сломанным и ходить по TCP, а Chrome ещё пробует QUIC и ловит ошибку посреди ответа. Для HTTP/2 разница в строгости: Chromium отклоняет ответ с несовпадающим Content-Length, другой движок может дочитать его до закрытия соединения. Сломан сервер или путь, а не браузер.
QUIC — это UDP?
QUIC работает поверх UDP, но сам делает всё, чего в UDP нет: подтверждения, повторную отправку, контроль перегрузки, потоки и шифрование. UDP нужен, чтобы пакеты проходили через существующие маршрутизаторы и NAT — новый протокол уровня IP интернет бы не пропустил. Для файрвола QUIC — поток UDP на порт 443, и именно правила для UDP решают, будет ли HTTP/3 работать.
Как отключить QUIC на сервере, не ломая сайт?
В nginx уберите add_header Alt-Svc во всех location, сделайте reload и подождите срок ma — браузеры перестанут пытаться открыть QUIC. Затем уберите listen ... quic и http3 on. Если снять listen quic сразу, браузеры откатятся на TCP, но соединения до истечения кэша будут медленнее. В Cloudflare достаточно выключить переключатель HTTP/3.