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:
proxy_http_version 1.1;— il default di nginx perproxy_passè HTTP/1.0, che non supporta l'upgrade a WebSocket. Senza questa riga, le due direttive Upgrade/Connection sopra non hanno alcun effetto.$http_upgradeè una variabile, non un valore fisso: nginx la valorizza copiando l'headerUpgradedella richiesta in arrivo. ScrivereUpgrade "websocket"a mano invece che$http_upgradefunziona solo finché il client chiede esattamente websocket, ma rompe il fallback automatico di SignalR su altri trasporti quando il WebSocket non è disponibile.- Gli header
X-Forwarded-*non servono per il WebSocket in sé, ma sono comunque da includere sempre quando Kestrel è dietro un proxy: senzaX-Forwarded-For, l'app vede tutte le richieste come provenienti da127.0.0.1(l'IP di nginx), il che rompe qualunque logica lato applicativo basata sull'IP reale del client — rate limiting per-IP compreso.
Come verificare che funzioni davvero
Non basta che la pagina si carichi: bisogna verificare che il circuito Blazor sia effettivamente interattivo.
- Apri l'app nel browser con la DevTools aperta, tab Network, filtro WS (WebSocket).
- 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. - 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).
- Lato server,
journalctl -u <nome-servizio> -fmentre interagisci con la pagina: dovresti vedere log applicativi corrispondenti alle azioni, non silenzio totale.
Errori comuni
- Testare da un client con VPN split-tunnel attiva verso un dominio che risolve pubblicamente. Se il dominio dell'app risolve all'IP pubblico del server ma quell'IP è fuori dal range instradato dalla VPN, il traffico esce comunque su internet normale — il sintomo sembra un problema di WebSocket ma è un problema di routing di rete a monte, non di configurazione nginx.
- Un certificato TLS non ancora emesso mentre si testa in HTTPS. Un cookie di sessione marcato
Securenon viene salvato dal browser su una connessione HTTP pura: il login sembra riuscire (risposta 200) ma la sessione non regge, sintomo facilmente confuso con un problema di WebSocket quando in realtà è un problema di certificato. - Dimenticare
proxy_http_version 1.1;copiando solo le due righe Upgrade/Connection da un esempio trovato online senza il resto del blocco.
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).