markdown
HTTP-QUIC-a-WebSocket.md
markdown
# HTTP, QUIC a WebSocket Verze HTTP jsou téma, kde je hodně marketingu a málo rozdílu v tom, co musíš doopravdy nastavit. Tahle stránka se drží toho druhého: co se mezi verzemi změnilo, co z toho poznáš na svém serveru a co je dneska mrtvé, i když to návody pořád doporučují. Navazuje na [porty a spojení](Porty-a-spojeni) a [TLS](TLS-a-HTTPS), nastavení konkrétních proxy je v [reverse proxy](Reverse-proxy). ## Rychlý přehled | | HTTP/1.1 | HTTP/2 | HTTP/3 ||---|---|---|---|| Rok | 1997 | 2015 | 2022 || Transport | TCP | TCP | **QUIC nad UDP** || Formát | text | binární | binární || Souběžné požadavky | ~6 spojení | jedno spojení, mnoho proudů | jedno spojení, mnoho proudů || Blokování při ztrátě paketu | částečné | **horší než u 1.1** | vyřešené || Šifrování | volitelné | v praxi povinné | povinné, součást protokolu || Přežije přechod z WiFi na mobil | ne | ne | ano | Aktuální specifikace jsou RFC 9110–9114 z roku 2022. Když návod cituje RFC 2616 nebo 7540, je zastaralý — což je docela slušný test kvality. ## HTTP/1.1 Textový protokol, jeden požadavek po druhém. Původní HTTP zavíralo spojení po každém souboru, takže se pro každý obrázek dělal nový TCP handshake. HTTP/1.1 přineslo **keep-alive**: spojení zůstane otevřené a pošle se po něm víc požadavků za sebou. Pořád ale platí, že **po jednom spojení běží jeden požadavek naráz**. Než dorazí odpověď, další se neposílá. Řešením mělo být pipelining — poslat víc dotazů bez čekání — jenže odpovědi se musely vracet ve stejném pořadí, takže jedna pomalá zablokovala všechny za sebou. Prohlížeče ho postupně vypnuly a dnes ho neumí ani `curl`. Místo toho prohlížeče otevírají **zhruba šest spojení na doménu**. Odtud pochází dvojice praktik, které v roce 2026 už jen škodí: - **Domain sharding** — rozházet obrázky na `img1.example.cz`, `img2.example.cz`, aby se limit obešel. Dnes to znamená DNS dotazy a TLS handshaky navíc.- **Sprite sheets a obří balíky** — slepit dvacet ikon do jednoho souboru. Rozbíjí to keš: změníš jednu ikonu a klient stahuje celý plát znovu. Obojí existovalo jen kvůli tomu limitu. S HTTP/2 limit zmizel a zůstala jen ta nevýhoda. ## HTTP/2 Stejné HTTP, jiný přenos. Hlavičky i těla se posílají v **binárních rámcích** a rámce z různých požadavků se prokládají po jednom TCP spojení. Tomu se říká multiplexování a je to celý smysl verze 2. K tomu **HPACK**, komprese hlaviček. Když posíláš třicet požadavků na stejný web, opakují se v nich stejné cookies a `User-Agent` — HPACK je pošle jednou. **Souběžných proudů není neomezeně.** Server ohlásí `SETTINGS_MAX_CONCURRENT_STREAMS`, v nginxu je výchozí hodnota 128. Nad ni se čeká úplně stejně jako dřív. Je to vysoký strop, ne žádný strop. ### Prohlížeč vyžaduje TLS, protokol ne Specifikace HTTP/2 bez šifrování povoluje (říká se tomu `h2c`), ale **žádný prohlížeč to nikdy neimplementoval**. Pro provoz z prohlížeče je tedy HTTP/2 vždycky přes HTTPS. `h2c` přesto potkáš — mezi proxy a aplikací, hlavně u gRPC. O tom, jestli se použije HTTP/2, se rozhoduje během TLS handshake mechanismem **ALPN**: klient v úvodním pozdravu pošle seznam protokolů, které umí, server jeden vybere. Nedomluví-li se, jede se po staru. ```bashopenssl s_client -alpn h2 -connect example.cz:443 < /dev/null 2>&1 | grep ALPN``` ### Zádrhel, který zůstal Tohle je nejdůležitější odstavec na stránce, protože se skoro všude tvrdí opak. **HTTP/2 odstranilo blokování na úrovni HTTP, ale ne na úrovni TCP.** A svůj dopad dokonce zhoršilo. TCP předává aplikaci jeden souvislý proud bajtů **v pořadí**. Když se po cestě ztratí jeden segment, jádro drží všechna už doručená pokračování v bufferu, dokud se díra nezaplní opakovaným odesláním. Jádro netuší, že nad tím existují nějaké proudy HTTP/2 — pozdržená data přitom obvykle patří úplně jiným požadavkům, které s tou ztrátou nemají nic společného. Důsledek: **jeden ztracený paket zastaví všechny souběžné požadavky.** U HTTP/1.1 se šesti spojeními zabrzdí zhruba šestina provozu, u HTTP/2 s jedním spojením se zastaví všechno. Proto HTTP/2 na špatné WiFi nebo na přetížené mobilní síti dokáže být **pomalejší než HTTP/1.1**. Není to chyba v nastavení, je to vlastnost TCP a spravit se to uvnitř TCP nedá. Přesně kvůli tomu vzniklo QUIC. ## HTTP/3 a QUIC HTTP/3 je HTTP/2 postavené na novém transportu. Ten transport se jmenuje **QUIC**, běží nad **UDP** a řeší přesně tu věc z předchozího odstavce. **Proč UDP?** Ne proto, že by byl UDP rychlejší. Nový transportní protokol vedle TCP by v praxi nešlo nasadit — routery, firewally a krabice poskytovatelů by pakety, kterým nerozumí, zahodily. UDP je jediná cesta, jak dostat nový transport skrz existující internet. QUIC si spolehlivost, potvrzování a řízení toku dělá sám nad ním. Co z toho plyne: **Proudy jsou záležitost transportu.** QUIC ví, které bajty patří kterému požadavku, takže ztráta paketu zdrží jen ten požadavek, kterého se týká. Ostatní běží dál. **TLS 1.3 je součástí protokolu.** Nešifrovaný QUIC neexistuje. Navázání spojení navíc splyne s TLS handshakem, takže místo „TCP handshake, potom TLS handshake" je to jedno kolo. **Spojení přežije změnu adresy.** QUIC identifikuje spojení podle Connection ID, ne podle čtveřice adres a portů. Telefon, který přejde z WiFi na mobilní data, si spojení i s TLS stavem podrží. Tohle je v praxi ta nejhmatatelnější výhoda — a pro homelab to znamená plynulejší Immich nebo Jellyfin na telefonu cestou z domu. ### Na co si dát pozor u 0-RTT Při opětovném připojení umí QUIC poslat data hned v prvním paketu. Zní to skvěle, ale **taková data jdou zopakovat**: kdo je odposlechne, může je poslat znovu a server je zpracuje podruhé. Smí tudy jít jen požadavky, u kterých opakování nevadí. Zapnout `ssl_early_data` před aplikací, která mění stav na `GET`, je bezpečnostní chyba, ne ladění výkonu. První připojení navíc žádné 0-RTT není, to je vždycky jedno kolo. ### Ani HTTP/3 není bez blokování Aby to bylo poctivé: - Uvnitř **jednoho** proudu se pořadí drží dál. Ztráta uprostřed velké odpovědi tu odpověď pořád zdrží.- **QPACK** (komprese hlaviček) může zdržet hlavičky, které odkazují na položky tabulky, jež ještě nedorazily.- Ztráta paketu pořád zmenší okno zahlcení celému spojení. Proudy se nezastaví, ale všechny se zpomalí. ## Jak prohlížeč pozná, že umíš HTTP/3 Tohle většina lidí přehlédne a pak se diví, proč se HTTP/3 „nezapnulo". **Alt-Svc.** Server pošle v odpovědi hlavičku: ```Alt-Svc: h3=":443"; ma=86400``` Znamená to „příště to zkus přes HTTP/3 na portu 443". Háček: je to hlavička **v odpovědi**, takže první načtení stránky proběhlo přes HTTP/2. HTTP/3 se použije až od druhého spojení. Přes nešifrované HTTP se Alt-Svc navíc ignoruje. **DNS záznam HTTPS.** Novější a lepší způsob — do DNS se dá záznam typu `HTTPS` se seznamem protokolů: ```bashdig +short example.cz HTTPS# 1 . alpn="h3,h2" ipv4hint=...``` Prohlížeč pak jede přes HTTP/3 **hned napoprvé**. V homelabu tohle skoro nikdo nenastavuje, takže „mám zapnuté HTTP/3" v praxi obvykle znamená „od druhého požadavku". ## Zapnutí v praxi Záleží hlavně na tom, jakou proxy už máš. ### Caddy Nic nedělej, [Caddy](Caddy) má HTTP/3 zapnuté od verze 2.6. Sám otevře QUIC a sám posílá Alt-Svc. Tvoje jediná práce je pustit dovnitř UDP: ```yamlports: - "443:443" - "443:443/udp" # bez tohohle HTTP/3 nefunguje``` ### Traefik Od verze 3 už to není experimentální funkce, stačí: ```yamlentryPoints: websecure: address: ":443" http3: {}``` ### Nginx Tady je to pracné a **selhává tiše**. Musí sedět pět věcí naráz: ```nginxserver { listen 443 ssl; listen 443 quic reuseport; # tohle je ten skutečný přepínač http2 on; add_header Alt-Svc 'h3=":443"; ma=86400' always;}``` 1. Binárka přeložená s `--with-http_v3_module` — ověříš `nginx -V`2. `listen ... quic` navíc k `listen ... ssl`3. `reuseport` právě jednou na danou adresu a port, ne v každém bloku4. Ručně přidaná hlavička `Alt-Svc` — **nginx ji sám neposílá**5. Průchozí UDP 443 Pozor na direktivu `http3 on;`, kterou návody uvádějí jako řešení: ta je zapnutá už ve výchozím stavu a sama o sobě neudělá nic. Rozhoduje `quic` u `listen`. Klasický výsledek je server, který má modul přeložený, UDP proražené i nějaké to `http3_*` ladění v konfiguraci — a pořád jede po HTTP/2, protože v `listen` chybí `quic`. Nikde se přitom neobjeví žádná chyba. ### Ověřování UDP **`nc -zvu host 443` nepoužívej.** UDP je nespojovaný, takže `nc` hlásí úspěch pokaždé, když nedostane zpět „port unreachable" — což je přesně to, co dělá firewall, který pakety mlčky zahazuje. Rozliší „otevřeno" od „černá díra" asi tak nikdy. Spolehlivé je jedině skutečné navázání QUIC spojení zvenku: prohlížeč a ve vývojářských nástrojích sloupec Protocol, nebo `curl` s podporou HTTP/3. **Systémový `curl` na Ubuntu 24.04 HTTP/3 neumí** a `--http3` rovnou odmítne. Je přeložený s `nghttp2`, ale bez `ngtcp2`. Lidé to čtou jako chybu serveru. Potřebuješ `curl` z kontejneru nebo přeložený s podporou QUIC. A `openssl s_client -alpn h3` fungovat nemůže v principu, protože `h3` se domlouvá až uvnitř QUIC. ## Stojí HTTP/3 za to? Poctivá odpověď: **HTTP/1.1 → HTTP/2 je ten upgrade, na kterém záleží. HTTP/3 je třešnička.** Zapni ho, když už ho tvoje proxy umí sama (Caddy, Traefik). Je to práce na minutu a nemáš důvod ho vypínat. Nepřekládej si kvůli němu nginx ze zdrojáků. Pět kroků, z nichž každý selže potichu, za velmi malý přínos. **Kde vyhraje:** telefon na mobilních datech, špatná WiFi v hotelu, linky s velkým zpožděním a hlavně přechody mezi sítěmi. **Kde nepřinese nic, nebo uškodí:** přístup po LAN, gigabitový optický přípoj a všechno, co už teď jde přes [WireGuard](WireGuard) — ten je sám o sobě UDP a šifrovat šifrované je práce navíc. Na slabém železe navíc QUIC stojí zhruba dvojnásobek až trojnásobek procesorového času na bajt, protože běží v uživatelském prostoru a nemá odlehčení od síťové karty, které TCP dostává zadarmo. Kdo sotva utáhne gigabit přes HTTPS, si HTTP/3 nepomůže. Firemní a hotelové sítě někdy UDP 443 blokují. Nic se tím nerozbije, prohlížeč spadne zpátky na HTTP/2 — je to důvod udržovat HTTP/2 v pořádku, ne důvod HTTP/3 nezapínat. ## Mezi proxy a aplikací stačí HTTP/1.1 Otázka, která napadne každého: když mluvím s prohlížečem přes HTTP/2, nemám tak mluvit i s aplikací za proxy? **Ne, a není to chyba.** Standardní a správné uspořádání je h2/h3 ven ke klientovi a obyčejné HTTP/1.1 s keep-alive dovnitř k aplikaci. Všechno, co h2 a h3 přinášejí — multiplexování, komprese hlaviček, zotavení ze ztrát — řeší problémy dlouhé, ztrátové cesty přes internet. Skok z proxy do kontejneru má zpoždění pod milisekundu a nulovou ztrátovost; tam je keep-alive optimální a levnější. Výjimky, kdy na protokolu k aplikaci záleží, jsou tři: **gRPC**, dlouhá streamovaná spojení a WebSocket přes HTTP/2. Přes HTTP/3 nemluví s aplikací žádná proxy. ## Co je mrtvé Věci, které pořád najdeš v návodech: **Server push** (`http2_push_preload`). Chrome ho vypnul v roce 2022, Firefox odstranil v roce 2024, nginx direktivy zrušil ve verzi 1.25.1. Používej `Link: rel=preload`, případně `103 Early Hints`. Pro úplnost: pro HTTP/3 je push pořád ve specifikaci, jen ho nikdo neimplementoval. **Priority podle původní specifikace HTTP/2** (rámec `PRIORITY`, stromy závislostí). Zrušené. Náhradou je jednoduchá hlavička `priority: u=3, i`, kterou stejně nemáš co ladit. **Pipelining.** Vypnutý všude. **Domain sharding a sprite sheets.** Viz výš — dnes škodí. ## WebSocket Obousměrné spojení, po kterém můžou obě strany posílat kdykoliv. Používá ho všechno živé: Home Assistant, Proxmox konzole, chaty, upozornění v administracích. Začíná to jako obyčejný HTTP požadavek: ```httpGET /ws HTTP/1.1Host: home.example.czUpgrade: websocketConnection: UpgradeSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==``` Server odpoví `101 Switching Protocols` a **od té chvíle už to není HTTP**. Po tomtéž TCP spojení teče vlastní rámcový protokol. Jede po portu 443 jako všechno ostatní, jen se místo `https://` píše `wss://`. Z toho plyne to podstatné pro provoz: proxy musí pochopit, že má spojení přepnout a pak už do něj nesahat. ### Nginx Nejčastější zdroj rozbitých aplikací v celém homelabu: ```nginxmap $http_upgrade $connection_upgrade { default upgrade; '' close;} location / { proxy_pass http://127.0.0.1:8123; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_read_timeout 3600s;}``` Tři věci, které tam musí být: **`proxy_http_version 1.1`** — nginx k aplikaci historicky mluvil protokolem 1.0, který `Upgrade` neumí. Od verze 1.29.7 je výchozí hodnota konečně 1.1, ale na všem starším je ten řádek nutný. Nech ho tam; na novém nginxu neuškodí. **Ta dvojice hlaviček.** `map` je tam proto, aby se `Connection: upgrade` posílalo jen u skutečného WebSocketu. Napevno zapsané `Connection "upgrade"` u všech požadavků ruší keep-alive a některé aplikace se z něj zblázní. **`proxy_read_timeout`.** Výchozí hodnota je **60 sekund** a počítá se od poslední přijaté zprávy. WebSocket, kterým chvíli nic neteče, se tiše zavře. Projeví se to jako aplikace, která „po minutě nečinnosti přestane reagovat". ### Caddy a Traefik [Caddy](Caddy) i [Traefik](Traefik) to zvládají samy, bez jediného řádku navíc. Je to jeden z hlavních důvodů, proč se s nimi v homelabu žije líp. ### WebSocket nepotřebuje vlastní port Časté nedorozumění: `wss://` jede po 443 jako všechno ostatní a handshake je obyčejný HTTPS požadavek. **Když se stránka načte a WebSocket ne, není to firewallem ani port forwardingem** — je to nastavením proxy. Nikam nelez a oprav hlavičky. ### WebSocket přes HTTP/2 a HTTP/3 Původní WebSocket stojí na mechanismu `Upgrade`, který v HTTP/2 neexistuje. Doplnilo se to později rozšířeným `CONNECT`, jenže v praxi to **prakticky nikdo nepoužívá**: nginx to neumí vůbec a servery psané v Go, což jsou i Caddy a Traefik, to mají ve výchozím stavu vypnuté. Pro HTTP/3 je to specifikované taky a neumí to zatím žádný prohlížeč. Prakticky to znamená, že **tvůj WebSocket jede po HTTP/1.1 na vlastním TCP spojení**, i když web jinak běží po HTTP/2 nebo HTTP/3. Nevadí to a nemusíš s tím nic dělat. Jeden důsledek si ale zapamatuj: **nevypínej HTTP/1.1.** Občas se doporučuje nechat na serveru jen `h2` a `h3` jako modernější volbu — tím si spolehlivě zabiješ všechny WebSockety. Výchozí `protocols h1 h2 h3` v Caddy je správně a není co vylepšovat. ## Server-Sent Events Jednosměrná varianta: server posílá, klient poslouchá. Obyčejné HTTP s `Content-Type: text/event-stream`, které se nikdy neukončí. Prohlížeč se sám umí znovu připojit. Na upozornění a průběžné výpisy je to jednodušší a robustnější než WebSocket. Rozbíjí se to o **bufferování a kompresi na proxy** — dorazí buď všechno naráz na konci, nebo nic: ```nginxlocation /events { proxy_pass http://127.0.0.1:3000; proxy_buffering off; gzip off; proxy_cache off; proxy_read_timeout 3600s;}``` Pro pořádek: rozšířené tvrzení, že nginx události zadržuje vždycky, není pravda. Když aplikace odpovídá po částech (`Transfer-Encoding: chunked`), moderní nginx je posílá průběžně i se zapnutým bufferováním. Skuteční viníci jsou **komprese** a aplikace, které konec odpovědi značí zavřením spojení. `proxy_buffering off` to spraví v obou případech, takže ho nastav — ale ber to jako pojistku, ne jako opravu nginxu. Aplikace to umí vyřešit i sama hlavičkou `X-Accel-Buffering: no`. ## gRPC Jede výhradně po HTTP/2 a **potřebuje ho po celé cestě**. Proxy, která ukončí HTTP/2 a k aplikaci mluví po HTTP/1.1, gRPC rozbije — pro gRPC totiž žádný tvar po HTTP/1.1 neexistuje. V nginxu se místo `proxy_pass` použije `grpc_pass` (a na naslouchání musí být `http2 on`), v Caddy se aplikace zapíše jako `h2c://`, v Traefiku se službě nastaví schéma `h2c`. Nepleť si to s `proxy_http_version 2`, které nginx umí od verze 1.29.4. To je něco jiného, nemultiplexuje to a **rozbíjí to WebSockety**, protože není co přepínat. Do sdíleného úryvku konfigurace to nedávej. Tohle je ta výjimka z pravidla „dovnitř stačí HTTP/1.1" o dva odstavce výš. ## Dlouhá spojení a časovače WebSocket i SSE spadnou na tom, že je někdo po cestě považuje za mrtvé. Míst je několik: Hledej je v tomhle pořadí: **Proxy.** `proxy_read_timeout`, výchozích 60 s. Zdaleka nejčastější viník a jediný, kterého máš plně v ruce. **Tabulka spojení na routeru.** Tady se často chybuje: samotný Linux drží záznam o navázaném TCP spojení **5 dní**, takže na tvém serveru to skoro nikdy není. Domácí routery jsou ale přísnější (OpenWrt zhruba dvě hodiny) a [CGNAT](Poskytovatel-CGNAT-a-verejna-IP) u poskytovatele klidně minuty. U UDP je to podstatně kratší i na Linuxu — 30 sekund, u obousměrného provozu 120. Souvisí to s [NATem](NAT-a-port-forwarding): jakmile záznam zmizí, odpovědi nemají kam. **Cloudflare** ukončí odpověď, kterou server nepošle do 125 s, chybou `524`. Pevný limit pro WebSockety nezveřejňuje a sám doporučuje tlouct srdcem. Řešení je vždycky stejné: **posílat ping.** Pozor ale na to, kdo ho posílá — v prohlížeči to nejde. Rozhraní `WebSocket` v JavaScriptu umí jen `send()` a `close()`, žádné `ping()`. Prohlížeč na `Ping` od serveru automaticky odpoví, ale sám ho nikdy nezačne. **Tlukot srdce tedy musí vysílat server**, nebo si ho aplikace udělá vlastní běžnou zprávou přes `send()`. ## Diagnostika ```bash# jakou verzi server nabízícurl -I --http2 https://example.czcurl -sI https://example.cz | grep -i alt-svc # co se domluvilo v ALPNopenssl s_client -alpn h2 -connect example.cz:443 < /dev/null 2>&1 | grep ALPN # je v DNS záznam pro HTTP/3?dig +short example.cz HTTPS # WebSocket handshake ručněcurl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" \ -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \ -H "Sec-WebSocket-Version: 13" https://home.example.cz/ws``` U posledního příkazu chceš vidět `101 Switching Protocols`. Když přijde `200`, proxy požadavek nepřepnula a spolkla ho jako obyčejné HTTP. Ve vývojářských nástrojích prohlížeče si zapni ve výpisu požadavků sloupec **Protocol**. Je to nejrychlejší způsob, jak zjistit, co se doopravdy použilo — `http/1.1`, `h2`, nebo `h3`. | Problém | Příčina ||---|---|| Prohlížeč jede po HTTP/1.1 | server nenabízí `h2` v ALPN, nebo je před ním starší proxy || HTTP/3 se nepoužije | chybí `Alt-Svc`, u nginxu `quic` v `listen`, nebo neprochází UDP 443 || HTTP/3 až od druhého načtení | tak Alt-Svc funguje; napravíš záznamem HTTPS v DNS || `curl: option --http3: not supported` | systémový curl na Ubuntu HTTP/3 neumí, není to chyba serveru || WebSocket vrací `200` místo `101` | proxy nepřeposílá `Upgrade` a `Connection` || WebSocket nejde, stránka ano | vždycky proxy, nikdy firewall — `wss://` jede po 443 || Spojení padá po minutě | `proxy_read_timeout`, výchozích 60 s || SSE nedorazí nic | komprese, nebo aplikace neposílá po částech || gRPC hlásí protokolovou chybu | proxy převádí na HTTP/1.1, chybí `grpc_pass` || `502` hned | za proxy nic neposlouchá, špatný port nebo jméno kontejneru || `504` po chvíli čekání | aplikace žije, ale nestihla odpovědět do `proxy_read_timeout` || `499` v logu nginxu | klient zavřel spojení dřív, než přišla odpověď. Normální. || Na mobilu se přeruší při přechodu na WiFi | bez HTTP/3 se spojení naváže znovu |