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!

2 Responses

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

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

Leave a Reply

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