Retour au cours

infra / nginx

WebSocket proxying

Leçon 131 exercice

Explication

Ce que vous allez apprendre

  • Comprendre pourquoi une connexion WebSocket a des besoins de proxying différents du HTTP classique
  • Configurer les headers Upgrade et Connection correctement, via une map dédiée
  • Adapter les timeouts pour des connexions qui peuvent rester ouvertes des heures
  • Choisir un algorithme de répartition adapté quand plusieurs backends WebSocket sont derrière Nginx
  • Tester une connexion WebSocket depuis le terminal pour valider une configuration

Dans quel contexte ?

Une application de chat en temps réel utilise des WebSockets pour pousser instantanément les nouveaux messages aux utilisateurs connectés, sans qu'ils aient besoin de rafraîchir la page. Le serveur applicatif tourne derrière Nginx, comme le reste de l'application, mais une connexion WebSocket ne se comporte pas du tout comme une requête HTTP classique une fois établie.

D'abord, comprendre en quoi WebSocket diffère fondamentalement du HTTP classique

Une requête HTTP normale s'ouvre, reçoit une réponse, et se termine. Une connexion WebSocket, elle, démarre comme une requête HTTP ("handshake"), puis "monte en grade" (upgrade) vers un protocole bidirectionnel qui reste ouvert, potentiellement pendant des heures, pour laisser transiter des messages dans les deux sens à tout moment.

Une fois cette différence comprise, il faut transmettre le bon signal au backend

Le handshake WebSocket repose sur deux headers HTTP précis : Upgrade: websocket et Connection: Upgrade. Nginx doit les transmettre fidèlement au backend, ce qui nécessite une petite map dédiée ($connection_upgrade) car la valeur du header Connection doit varier selon que la requête est ou non une demande d'upgrade.

Il reste un piège très concret lié à la durée des connexions

Les timeouts par défaut de Nginx pour un proxy HTTP classique (souvent autour de 60 secondes) sont pensés pour des requêtes courtes. Une connexion WebSocket inactive pendant quelques minutes (l'utilisateur ne tape rien dans le chat) serait fermée prématurément par Nginx si les timeouts ne sont pas fortement augmentés — un cas d'usage complètement différent du HTTP classique.

RéglageHTTP classiqueWebSocket
proxy_read_timeoutQuelques secondes à quelques minutesSouvent 3600s ou plus
proxy_http_version1.0 ou 1.11.1 obligatoire
Durée de connexion typiqueQuelques centaines de millisecondesDes minutes, voire des heures

Prérequis

Cette leçon suppose que tu es à l'aise avec proxy_pass et les headers de proxy (leçon 4) : le proxying WebSocket ajoute des headers spécifiques par-dessus la base déjà connue.

Ensuite, une question se pose dès que plusieurs backends WebSocket existent

Si l'application ne partage pas l'état de ses connexions entre plusieurs instances (pas de session store commun), un client doit rester connecté au MÊME serveur backend pendant toute la durée de sa session WebSocket. ip_hash sur l'upstream garantit ce comportement, contrairement à un round robin qui pourrait router deux tentatives de connexion successives vers des serveurs différents.

Piège fréquent

Oublier d'augmenter proxy_read_timeout et proxy_send_timeout pour un endpoint WebSocket est l'erreur la plus fréquente : les utilisateurs constatent des déconnexions aléatoires après quelques dizaines de secondes d'inactivité, un symptôme souvent mal diagnostiqué comme un bug applicatif alors qu'il vient uniquement de la configuration Nginx par défaut.

Bonne pratique

Teste toujours une nouvelle configuration WebSocket avec un simple curl incluant les headers de handshake manuel, avant de brancher un vrai client applicatif : une réponse 101 Switching Protocols confirme que le proxying fonctionne au niveau HTTP, indépendamment de la logique métier de l'application.

Maintenant que le temps réel est maîtrisé, la prochaine leçon change d'échelle : le tuning de performance et la haute disponibilité pour un Nginx qui doit absorber un trafic élevé sans faiblir.

Commandes & code

WebSocket proxying

nginx
# Le map suivant est nécessaire pour transmettre correctement le header Connection lors d'un upgrade WebSocket
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;     # si pas de header Upgrade, on force la fermeture propre de la connexion
}

server {
    listen 443 ssl;
    server_name ws.example.com;

    location /socket/ {
        proxy_pass http://127.0.0.1:8001;

        proxy_http_version 1.1;                       # WebSocket nécessite HTTP/1.1 minimum
        proxy_set_header Upgrade $http_upgrade;         # transmet la demande d'upgrade du client
        proxy_set_header Connection $connection_upgrade; # "upgrade" pendant le handshake, sinon "close"

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;

        # Timeouts LONGS : une connexion WebSocket peut rester ouverte des heures
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}
nginx
# Load balancing de WebSocket : ip_hash recommandé si pas de mécanisme de reconnexion/state partagé côté app
upstream ws_backend {
    ip_hash;                    # un client reste sur le même serveur pendant toute la session WS
    server 10.0.0.11:8001;
    server 10.0.0.12:8001;
}

server {
    location /socket/ {
        proxy_pass http://ws_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
    }
}
python
# Exemple d'endpoint WebSocket FastAPI derrière ce proxy (rien de spécial à faire côté app)
from fastapi import FastAPI, WebSocket

app = FastAPI()

@app.websocket("/socket/chat")
async def chat(ws: WebSocket):
    await ws.accept()
    while True:
        data = await ws.receive_text()
        await ws.send_text(f"echo: {data}")
bash
# Tester une connexion WebSocket via curl (vérifie juste le handshake HTTP 101)
curl -i -N \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  https://ws.example.com/socket/chat
# doit répondre : HTTP/1.1 101 Switching Protocols

Résumé

  • Le trio proxy_http_version 1.1 + Upgrade + Connection (via un map) est OBLIGATOIRE pour proxifier du WebSocket.
  • Les timeouts par défaut (souvent 60s) coupent les connexions WS inactives : les augmenter fortement (3600s ou plus).
  • Sans session store partagé côté app, ip_hash (ou hash sur un ID de session) évite qu'un client bascule de serveur en cours de connexion.

Exercices pratiques

1 disponible
1

Mission : arrêter les déconnexions aléatoires d'un chat en temps réel

Objectif : Identifier pourquoi des connexions WebSocket se coupent après une courte inactivité et configurer un proxying WebSocket complet et résilient sur plusieurs backends.

Contexte

Une application de chat utilise des WebSockets derrière Nginx. Les utilisateurs signalent des déconnexions aléatoires après environ une minute d'inactivité dans la conversation. L'application tourne aussi sur deux instances backend (10.0.0.11:8001 et 10.0.0.12:8001) sans aucun store de session partagé entre elles.

Résoudre l’exercice →