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!

7 Responses

  1. Osqui
    Osqui November 23, 2018 at 4:05 pm | | Reply

    As far I know, there’s no Websockets in HTTP/2

  2. Mathieu
    Mathieu August 5, 2019 at 7:17 pm | | Reply

    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://

  3. Gina Therese Hvidsten
    Gina Therese Hvidsten September 29, 2019 at 7:49 am | | Reply

    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.

  4. Kevin Hilton
    Kevin Hilton October 5, 2019 at 6:51 am | | Reply

    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)

Leave a Reply

Your email address will not be published. Required fields are marked *