Shiny for Python app fails to load when behind reverse-proxy

Hello! I have found an interesting issue with the ShinyProxy Shiny for Python Demo: It does not work when ShinyProxy is behind a reverse-proxy: The static components load, but the WebSocket does not start. According to the container logs, the app itself is returning a 403 Forbidden error. I’d appreciate some assistance in identifying exactly where the problem is.

I’m running ShinyProxy version 3.0.2-exec, which I installed using the Debian package from the ShinyProxy downloads page.

Here's my ShinyProxy `application.yml` file:
proxy:
  title: ShinyProxy!
  logo-url: https://www.openanalytics.eu/shinyproxy/logo.png
  bind-address: 0.0.0.0
  landing-page: /
  heartbeat-rate: 10000
  heartbeat-timeout: 60000
  port: 8080
  authentication: simple
  users:
    - name: karl
      password: karl
      groups: admins
  admin-groups: admins
  docker:
    port-range-start: 20000
    image-pull-policy: always
  specs:
    - id: shinyproxy_python_demo
      display-name: ShinyProxy Python Demo
      description: The demo from https://github.com/openanalytics/shinyproxy-shiny-for-python-demo
      container-image: openanalytics/shinyproxy-shiny-for-python-demo
      port: 8080
server:
  forward-headers-strategy: native

As you can see, I’m not making any changes to the container, and I’m not building it myself.

I mentioned that I’m using an Apache reverse-proxy. I have Apache set up to listen in ports 80 & 443. Requests to port 80 simply redirect to port 443.

Here's my Apache configuration for port 443
<VirtualHost *:443>
ServerAdmin srcc-support@stanford.edu
DocumentRoot /var/www/html

ErrorLog /var/log/apache2/error.log
CustomLog /var/log/apache2/access.log combined

# Enable HTTP2 and 1.1
Protocols h2 http/1.1

# Enable SSL, with HSTS
SSLEngine on
SSLCertificateKeyFile /etc/ssl/private/apache2.key
SSLCertificateFile /etc/ssl/certs/bc-vm-hidplapps.pem
SSLCertificateChainFile /etc/ssl/certs/bc-vm-hidplapps.chain.pem
Header always set Strict-Transport-Security "max-age=63072000"

# Proxy requests through to ShinyProxy on port 8080
# NOTE: The WebSockets match must come first.
ProxyPassMatch "^/(.+)/websocket" "ws://127.0.0.1:8080/$1/websocket"
ProxyPass "/" "http://127.0.0.1:8080/"
ProxyPassReverse "/" "http://127.0.0.1:8080/"
ProxyPreserveHost On
RequestHeader set "X-Forwarded-Proto" https

I have Docker logs set to go to journald. Here’s what gets logged when I try to run the application:

[2024-06-13 21:50:58 +0000] [1] [INFO] Starting gunicorn 20.1.0
[2024-06-13 21:50:58 +0000] [1] [INFO] Listening at: http://0.0.0.0:8080 (1)
[2024-06-13 21:50:58 +0000] [1] [INFO] Using worker: uvicorn.workers.UvicornWorker
[2024-06-13 21:50:58 +0000] [7] [INFO] Booting worker with pid: 7
[2024-06-13 21:50:59 +0000] [7] [INFO] Started server process [7]
[2024-06-13 21:50:59 +0000] [7] [INFO] Waiting for application startup.
[2024-06-13 21:50:59 +0000] [7] [INFO] Application startup complete.
[2024-06-13 21:51:00 +0000] [7] [INFO] ('172.17.0.1', 34698) - "WebSocket /websocket" 403
[2024-06-13 21:51:00 +0000] [7] [INFO] connection failed (403 Forbidden)
[2024-06-13 21:51:00 +0000] [7] [INFO] connection closed

(edited to remove the stuff journald added to each line)

So, it seems to me like something in the Python app does not like that the request is coming in through a reverse-proxy.

You might ask, “How do you know the reverse-proxy is causing the problem?” You might also ask “Why is ShinyProxy listening on 0.0.0.0 instead of 127.0.0.1?” The answers are related: I configured ShinyProxy to listen on all IPs, so that I could test trying to connect from my web browser to ShinyProxy directly on port 8080, bypassing Apache. If I connect to ShinyProxy directly—bypassing Apache & the reverse-proxy—the container works fine.

So, it seems to me like the problem is definitely something in the container, and that’s where I need help: I don’t know if the problem is in the Shiny for Python code, or in one of its dependencies.

I’d appreciate any pointers you can provide!

Hi

Can you try using the example Apache configuration at https://shinyproxy.io/documentation/security/ ?
Especially the part on websockets:

RewriteEngine on
RewriteCond %{HTTP:Upgrade} =websocket
RewriteRule /(.*) ws://127.0.0.1:8080/$1 [P,L]

Thanks much, that was the problem! Besides the rewrite config you provided, I also needed to set:

RequestHeader set "X-Forwarded-Proto" expr=%{REQUEST_SCHEME}

Previously, I had hard-coded:

RequestHeader set "X-Forwarded-Proto" https

1 Like