# 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.

```bash
openssl 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ů:

```bash
dig +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:

```yaml
ports:
  - "443:443"
  - "443:443/udp"     # bez tohohle HTTP/3 nefunguje
```

### Traefik

Od verze 3 už to není experimentální funkce, stačí:

```yaml
entryPoints:
  websecure:
    address: ":443"
    http3: {}
```

### Nginx

Tady je to pracné a **selhává tiše**. Musí sedět pět věcí naráz:

```nginx
server {
    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 bloku
4. 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:

```http
GET /ws HTTP/1.1
Host: home.example.cz
Upgrade: websocket
Connection: Upgrade
Sec-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:

```nginx
map $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:

```nginx
location /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.cz
curl -sI https://example.cz | grep -i alt-svc

# co se domluvilo v ALPN
openssl 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 |
