Reverse proxying WebSocket requests with Apache: a generic approach that works (even with Firefox)

Right up front, I should say all credit for this goes to Patrick Uiterwijk - I am just writing it up :)

So I'm upgrading Fedora's openQA instances to the latest upstream code, which replaces the old 'interactive mode' with a new 'developer mode'. This relies on the browser being able to establish a WebSocket connection to the server.

Both my pet deployment of openQA and the official Fedora instances have some reverse proxying going on between the browser and the actual box where the openQA server bits are running, and in both cases, Apache is involved.

Some HTTP reverse proxies just magically pass WebSocket requests correctly, I've heard it said, but Apache does not.

The 'standard' way to do reverse proxying with Apache looks something like this:

<VirtualHost *:443>
    ServerName openqa.happyassassin.net
    ProxyPass / https://openqa-backend01/
    ProxyPassReverse / https://openqa-backend01/
    ProxyRequests off
</VirtualHost>

but that alone does not handle WebSocket requests correctly. AIUI, it sort of strips the WebSocket-y bits off and makes them into plain https requests, which the backend server then mishandles because, well, why are you sending plain https requests when you should be sending WebSocket-y ones?

If you've run into this before, and Googled around for solutions, what you've probably found is stuff which relies on knowing specific locations to which requests should always be WebSocket-y, and ProxyPassing those specifically, like this:

<VirtualHost *:443>
    ServerName openqa.happyassassin.net
    ProxyPass /liveviewhandler/ wss://openqa-backend01/liveviewhandler/
    ProxyPassReverse /liveviewhandler/ wss://openqa-backend01/liveviewhandler/
    ProxyPass / https://openqa-backend01/
    ProxyPassReverse / https://openqa-backend01/
    ProxyRequests off
</VirtualHost>

...which basically means 'if this is a request to a path under livehandler/, we know that ought to be a WebSocket request, so proxy it as one; otherwise, proxy as https'. And that works, you can do that. For every websocket-y path. For every application. So long as you can always distinguish between where websocket-y requests go and where plain http-y ones go.

But it seems like a bit of a drag! So instead, why not try this?

<VirtualHost *:443>
    ServerName openqa.happyassassin.net
    RewriteEngine on
    RewriteCond %{HTTP:Upgrade} websocket [NC]
    RewriteCond %{HTTP:Connection} upgrade [NC]
    RewriteRule .* "wss://openqa-backend01%{REQUEST_URI}" [P]
    ProxyPass / https://openqa-backend01/
    ProxyPassReverse / https://openqa-backend01/
    ProxyRequests off
</VirtualHost>

That takes advantage of mod_rewrite, and what it basically says is: if the HTTP Connection header has the string 'upgrade' in it, and the HTTP Upgrade header has the string 'websocket' in it - both case-insensitive, that's what the [NC] means - then it's a WebSocket request, so proxy it as one. Otherwise, proxy as plain https.

You may have seen similar incantations, but stricter, like this:

RewriteCond %{HTTP:Upgrade} ^WebSocket$
RewriteCond %{HTTP:Connection} ^Upgrade$

or case-insensitive but still rejecting other text before or after the key word, or loose about additional text but case-sensitive. If you tried something like that, you might've found it doesn't work with Firefox...and that's because Firefox, apparently, sends 'websocket' (not 'WebSocket') in the Upgrade header, and 'keep-alive, Upgrade' (not just 'Upgrade') in the Connection header. It seems that lots of stuff around WebSocket requests has been developed using Chrom(e|ium) as a reference, so assuming the headers will look just the way Chrome does them...but that's not the real world!

The magic recipe above should correctly proxy all WebSocket requests through to the backend server unmolested.

Thanks again to Patrick for this!

Comments

Osqui wrote on 2018-11-24 00:05:
As far I know, there's no Websockets in HTTP/2
adamw wrote on 2018-11-24 00:11:

That's...nice? This post wasn't about HTTP/2. So.

Mathieu wrote on 2019-08-06 02:17:
this worked nicely for me, only issue I had is my backend is not https but regular http, since its only local took me a while I had to specify ws:// and not wss://
Gina Therese Hvidsten wrote on 2019-09-29 14:49:
I also had to enable the "wstunnel" proxy module by enabling this line in httpd.conf LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so (can probably be done by "sudo a2enmod mode_proxy_wstunnel" or similar) But when I had done it these rewrite commands started also proxying my websocket requests.
adamw wrote on 2019-10-18 18:09:
Yeah, you will need that module enabled. IIRC on Fedora it is enabled by default but perhaps not on other distros. Thanks for the note!
Kevin Hilton wrote on 2019-10-05 13:51:
Hi -- thanks a lot for this information. Any insight in how I could possibly make a ws/wss connection through two Apache reverse proxies? I currently have Internet ---->(HTTPS) -----> Apache Reverse Proxy ---->(HTTPS)---->Apache Revers Proxy ---->(HTTP)/(WS)
adamw wrote on 2019-10-18 18:09:
Sorry, I haven't dealt with that config before :/
Guilherme wrote on 2020-02-19 15:55:
I had to do the same thing, what worked for me is: ProxyPreserveHost On SSLProxyEngine On RewriteEngine on RewriteCond %{HTTP:Upgrade} websocket [NC] RewriteCond %{HTTP:Connection} upgrade [NC] RewriteRule .* "wss://%{REQUEST_URI}" [P] What is important to notice are the 2 extra flags I am using before the RewriteRule. ProxyPreserveHost guarantees the response to the client will not seem like a redirect, I was getting code 302 on client without it. SSLProxyEngine let the SSL handshake be handled by the 2° Apache, you won't be able to proxy an WSS without this flag. Hope this help you if you still needs it.
Glenn Matthys wrote on 2019-12-14 10:26:
Nice work.
Jonathan Bennett wrote on 2020-01-20 22:52:
Just in case someone else has the same problem I did, the order matters. Meaning, if you have the "proxypass /" line first, it will get higher priority, and your ws requests won't get proxied.
Frederick Henderson wrote on 2020-01-29 22:11:
Thanks! This worked great. Also works for non-SSL secured connections just change the virtualhost port to 80 and "wss" to "ws" and your go to go.