Description
WebSocket connections through NPM fail when using HTTPS, returning 400 Bad Request from the backend. The same connection works correctly over HTTP. The root cause is that NPM negotiates HTTP/2 with the client
on HTTPS, but the upstream proxy_pass also uses HTTP/2 (when backend supports it), which breaks the classic WebSocket upgrade mechanism (Connection: Upgrade / Upgrade: websocket headers are not valid in
HTTP/2).
Environment
- NPM version: 2.14.0 (Docker: jc21/nginx-proxy-manager:2.14.0)
- Backend: NanoKVM (HTTPS, supports both HTTP/1.1 and HTTP/2)
- WebSocket endpoint: wss://example.com/api/ws
- Proxy Host config: Scheme https, Websockets Support enabled
Steps to Reproduce
- Create a Proxy Host with scheme https, enable Websockets Support and SSL with HTTP/2 enabled
- Backend serves HTTPS and supports HTTP/2
- Attempt a WebSocket connection:
FAILS — curl negotiates HTTP/2 over HTTPS
curl -k -i -N
-H "Connection: Upgrade"
-H "Upgrade: websocket"
-H "Sec-WebSocket-Version: 13"
-H "Sec-WebSocket-Key: dGVzdA=="
https://example.com/api/ws
Response:
HTTP/2 400
content-type: text/plain; charset=utf-8
sec-websocket-version: 13
content-length: 12
Bad Request
Expected Behavior
WebSocket upgrade should succeed with 101 Switching Protocols.
Workarounds That Confirm the Cause
Workaround 1: Force HTTP/1.1 on the client side — works:
curl -k -i -N --http1.1
-H "Connection: Upgrade"
-H "Upgrade: websocket"
-H "Sec-WebSocket-Version: 13"
-H "Sec-WebSocket-Key: dGVzdA=="
https://example.com/api/ws
Response:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: upgrade
Sec-WebSocket-Accept: ...
Workaround 2: Use HTTP instead of HTTPS — works (because HTTP doesn't negotiate HTTP/2).
Workaround 3: Disable HTTP/2 Support in the SSL tab — works but disables HTTP/2 for all traffic.
Root Cause Analysis
When Websockets Support is enabled, NPM adds:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
But it does not add:
proxy_http_version 1.1;
Without proxy_http_version 1.1;, Nginx may use HTTP/2 for the upstream connection (when the backend supports it). In HTTP/2, the Connection and Upgrade headers are prohibited (RFC 7540 §8.1.2.2), so the
WebSocket handshake never reaches the backend correctly.
Suggested Fix
When Websockets Support is toggled on, NPM should also inject proxy_http_version 1.1; into the generated location block. This ensures the upstream connection uses HTTP/1.1, which is required for the classic
WebSocket upgrade handshake.
Note: Adding proxy_http_version 1.1; manually in the Custom Nginx Configuration (Advanced tab) at the server level causes a config syntax error and sets the host Offline, because this directive is only valid
inside a location block.
Description
WebSocket connections through NPM fail when using HTTPS, returning 400 Bad Request from the backend. The same connection works correctly over HTTP. The root cause is that NPM negotiates HTTP/2 with the client
on HTTPS, but the upstream proxy_pass also uses HTTP/2 (when backend supports it), which breaks the classic WebSocket upgrade mechanism (Connection: Upgrade / Upgrade: websocket headers are not valid in
HTTP/2).
Environment
Steps to Reproduce
FAILS — curl negotiates HTTP/2 over HTTPS
curl -k -i -N
-H "Connection: Upgrade"
-H "Upgrade: websocket"
-H "Sec-WebSocket-Version: 13"
-H "Sec-WebSocket-Key: dGVzdA=="
https://example.com/api/ws
Response:
HTTP/2 400
content-type: text/plain; charset=utf-8
sec-websocket-version: 13
content-length: 12
Bad Request
Expected Behavior
WebSocket upgrade should succeed with 101 Switching Protocols.
Workarounds That Confirm the Cause
Workaround 1: Force HTTP/1.1 on the client side — works:
curl -k -i -N --http1.1
-H "Connection: Upgrade"
-H "Upgrade: websocket"
-H "Sec-WebSocket-Version: 13"
-H "Sec-WebSocket-Key: dGVzdA=="
https://example.com/api/ws
Response:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: upgrade
Sec-WebSocket-Accept: ...
Workaround 2: Use HTTP instead of HTTPS — works (because HTTP doesn't negotiate HTTP/2).
Workaround 3: Disable HTTP/2 Support in the SSL tab — works but disables HTTP/2 for all traffic.
Root Cause Analysis
When Websockets Support is enabled, NPM adds:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
But it does not add:
proxy_http_version 1.1;
Without proxy_http_version 1.1;, Nginx may use HTTP/2 for the upstream connection (when the backend supports it). In HTTP/2, the Connection and Upgrade headers are prohibited (RFC 7540 §8.1.2.2), so the
WebSocket handshake never reaches the backend correctly.
Suggested Fix
When Websockets Support is toggled on, NPM should also inject proxy_http_version 1.1; into the generated location block. This ensures the upstream connection uses HTTP/1.1, which is required for the classic
WebSocket upgrade handshake.
Note: Adding proxy_http_version 1.1; manually in the Custom Nginx Configuration (Advanced tab) at the server level causes a config syntax error and sets the host Offline, because this directive is only valid
inside a location block.