Skip to content

WebSocket connections fail over HTTPS (HTTP/2) — 400 Bad Request from backend #5503

@geekidentity

Description

@geekidentity

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

  1. Create a Proxy Host with scheme https, enable Websockets Support and SSL with HTTP/2 enabled
  2. Backend serves HTTPS and supports HTTP/2
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions