2025, Oct 20 17:00

Resolve Flask static file 404s behind IIS by preserving the virtual directory and mounting with DispatcherMiddleware

Static files 404 behind IIS? Fix Flask under a virtual directory with URL Rewrite and DispatcherMiddleware; preserve subpaths and stop IIS asset interception.

When you deploy a Flask app behind IIS using URL Rewrite, static assets can suddenly disappear. HTML renders, but css, js, and images return 404. Locally everything works, the app runs fine with waitress, yet the moment it’s proxied under a virtual directory such as https://myhost/MyApp, asset paths stop resolving.

Reproducing the issue

The project layout places templates and static assets under a frontend directory, while the Flask backend lives under src/backend. The app is configured with a custom static folder and a static_url_path.

import os as osmod
import flask as fk
from werkzeug.middleware.proxy_fix import ProxyFix as ProxyAdaptor

APP_ROOT = '/MYAPP'

tpl_path = osmod.path.abspath(osmod.path.join(osmod.path.dirname(__file__), '../frontend/templates'))
asset_base = osmod.path.abspath(osmod.path.join(osmod.path.dirname(__file__), '../frontend'))

svc = fk.Flask(
    __name__,
    template_folder=tpl_path,
    static_folder=asset_base,
    static_url_path=f'{APP_ROOT}/src/frontend'
)
svc.config['APPLICATION_ROOT'] = APP_ROOT
svc.wsgi_app = ProxyAdaptor(
    svc.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1
)

@svc.route('/')
def index():
    return fk.render_template('page.html')

@svc.route('/toMYAPP')
def go_myapp():
    return fk.render_template('page.html')

The template references static files through url_for, so Flask should serve them from the configured static folder.

<link rel="stylesheet" href="{{ url_for('static', filename='css/styles.css') }}">
<script src="{{ url_for('static', filename='js/my_app_js.js') }}"></script>
<img src="{{ url_for('static', filename='images/image1.png') }}">
<img src="{{ url_for('static', filename='images/image2.png') }}">

In IIS, a virtual directory uses a reverse proxy rule to forward requests to the waitress server.

<configuration>
  <system.webServer>
    <rewrite>
      <rules>
        <rule name="ReverseProxyToFlask" stopProcessing="true">
          <match url="^(.*)$" />
          <action type="Rewrite" url="http://localhost:8081/{R:1}" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>

What actually breaks

The reverse proxy omits the /MyApp segment when forwarding. The backend receives paths that no longer include the virtual directory prefix, so all asset URLs generated for the static endpoint don’t match what IIS is sending through. That’s why css, js, and images return 404 while the template itself renders.

Fixing the path mismatch cleanly

The working approach is to make the reverse proxy include the virtual directory in the forwarded URL, and mount the Flask app under that same subpath at the WSGI level. This aligns what IIS sends and what the app expects.

Mount the application under /MyApp using DispatcherMiddleware.

from werkzeug.middleware.dispatcher import DispatcherMiddleware as Mux

# keep the Flask instance defined earlier as `svc`
application = Mux(None, {
    '/MyApp': svc
})

With this in place, the app is explicitly served under /MyApp. The ProxyFix middleware becomes unnecessary.

Update the IIS rewrite rule so the subpath is preserved, and ensure IIS does not intercept static file extensions intended for Flask.

<configuration>
  <system.webServer>
    <rewrite>
      <rules>
        <rule name="ReverseProxyToFlask" stopProcessing="true">
          <match url=".*" />
          <action type="Rewrite" url="http://127.0.0.1:8081/MyApp/{R:0}" />
        </rule>
      </rules>
    </rewrite>
    <staticContent>
      <remove fileExtension=".css" />
      <remove fileExtension=".js" />
      <remove fileExtension=".png" />
    </staticContent>
  </system.webServer>
</configuration>

Finally, run waitress and expose the WSGI entry that includes the mounted app.

> cd MYAPP/src/backend
> waitress-serve --host 127.0.0.1 --port=8081 app:application

Why this matters

When an app is deployed under a subpath behind a reverse proxy, the upstream and downstream must agree on the prefix. If the proxy strips the prefix while the app generates URLs that include it, asset resolution fails. Aligning the path at both the proxy and WSGI layers prevents 404s for static files and keeps routing predictable.

Takeaways

If static files work locally but break behind IIS, check whether the virtual directory segment is being dropped. Mount the Flask app under the same subpath using DispatcherMiddleware and adjust the URL Rewrite rule to pass that segment through. If IIS is grabbing static file extensions, remove those mappings so the backend can serve the assets. With these adjustments, templates, css, js, and images resolve consistently under the virtual directory.

The article is based on a question from StackOverflow by Juan Calvo Franco and an answer by Juan Calvo Franco.