Configurare SignalR dietro nginx senza perdere la connessione WebSocket

Come configurare nginx come reverse proxy davanti a un'app Blazor Server/SignalR senza che il circuito cada: le direttive header necessarie e gli errori più comuni.

Pubblicato il 16 settembre 20264 min di lettura

Per far funzionare SignalR (e quindi un'app Blazor Server) dietro nginx servono due direttive header nel blocco location: proxy_set_header Upgrade $http_upgrade; e proxy_set_header Connection "upgrade";. Senza queste due righe, nginx tratta la richiesta come una normale connessione HTTP e la interrompe dopo la risposta iniziale — la pagina si carica ma resta "morta": nessun bottone risponde, nessun binding aggiorna lo stato, senza nessun errore visibile in console.

Perché succede

SignalR (il layer di comunicazione realtime sotto Blazor Server) prova prima una connessione WebSocket e, se non disponibile, fa fallback su altri trasporti. Un WebSocket nasce come richiesta HTTP che poi "sale di livello" tramite gli header Upgrade/Connection — è un protocollo diverso da quel momento in poi, a lunga durata, non una singola richiesta/risposta. Un reverse proxy configurato con le impostazioni HTTP di default non sa che quella specifica connessione va tenuta aperta e inoltrata byte per byte: la chiude come farebbe con qualsiasi altra richiesta.

Apache ha lo stesso problema ma la soluzione è più macchinosa: richiede il modulo mod_proxy_wstunnel più una RewriteCond dedicata per instradare la richiesta al modulo giusto. nginx gestisce l'upgrade nativamente, bastano le due direttive header.

Configurazione minima che funziona

Blocco location per un'app Blazor Server dietro nginx (esempio con Kestrel in ascolto su 127.0.0.1:5102, sottodominio dedicato all'app):

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

    # ... direttive ssl_certificate, TLS, ecc.

    location / {
        proxy_pass http://127.0.0.1:5102;
        proxy_http_version 1.1;

        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Tre dettagli che non sono opzionali, anche se a volte vengono dimenticati:

Come verificare che funzioni davvero

Non basta che la pagina si carichi: bisogna verificare che il circuito Blazor sia effettivamente interattivo.

  1. Apri l'app nel browser con la DevTools aperta, tab Network, filtro WS (WebSocket).
  2. Ricarica la pagina: deve comparire una richiesta con Status 101 Switching Protocols verso /_blazor (o l'hub SignalR configurato). Se vedi 200 o la richiesta non compare proprio, l'upgrade non sta avvenendo.
  3. Interagisci con un elemento della pagina che richiede un round-trip al server (un bottone che aggiorna uno stato, una validazione). Se non risponde nonostante lo status 101, il problema è altrove (es. errore applicativo nei log, non più nel proxy).
  4. Lato server, journalctl -u <nome-servizio> -f mentre interagisci con la pagina: dovresti vedere log applicativi corrispondenti alle azioni, non silenzio totale.

Errori comuni

Domande frequenti

Serve una configurazione diversa se ho più app Blazor Server dietro lo stesso nginx? No, ogni server_name/location che fa da proxy verso un'app Blazor Server ha bisogno delle stesse direttive, indipendentemente da quante app coesistono sulla stessa macchina nginx.

Il problema può essere lato Kestrel invece che lato nginx? Raramente, se l'app gira correttamente in locale senza proxy davanti. Se il WebSocket fallisce anche in locale, il problema è nell'app (es. CORS mal configurato, middleware che intercetta la richiesta prima dell'hub SignalR), non nel reverse proxy.

Devo configurare qualcosa di diverso per SignalR "puro" (non Blazor Server, es. una chat realtime)? No, la configurazione nginx è identica: SignalR usa lo stesso meccanismo di upgrade indipendentemente da cosa gira sopra (circuito Blazor o un hub applicativo custom).