2025, Oct 20 17:16

Исправляем 404 статических файлов во Flask за IIS: URL Rewrite и виртуальный каталог

Статика во Flask за IIS исчезает? Разбираем 404 из‑за URL Rewrite и виртуального каталога, показываем настройку reverse proxy и DispatcherMiddleware.

Когда вы разворачиваете приложение Flask за IIS с URL Rewrite, статические ресурсы могут внезапно исчезнуть. HTML отрисовывается, но css, js и изображения возвращают 404. Локально всё работает, приложение без проблем запускается с waitress, однако как только его проксируют под виртуальным каталогом вроде https://myhost/MyApp, пути к ресурсам перестают разрешаться.

Как воспроизвести проблему

Структура проекта хранит шаблоны и статические файлы в каталоге frontend, а бэкенд Flask — в src/backend. Приложение настроено на пользовательскую папку статических файлов и 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')

В шаблоне статические файлы запрашиваются через url_for, поэтому Flask должен отдавать их из настроенной папки static.

<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') }}">

В IIS виртуальный каталог использует правило обратного прокси для переадресации запросов на сервер waitress.

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

Что на самом деле ломается

Обратный прокси опускает сегмент /MyApp при пересылке. Бэкенд получает пути без префикса виртуального каталога, поэтому все URL ресурсов, которые генерируются для статической конечной точки, не совпадают с тем, что IIS фактически передаёт. Отсюда 404 для css, js и изображений, хотя сам шаблон отрисовывается.

Как аккуратно устранить несоответствие путей

Рабочее решение — заставить обратный прокси сохранять виртуальный каталог в пересылаемом URL и смонтировать приложение Flask под тем же подпутём на уровне WSGI. Так то, что отправляет IIS, будет согласовано с ожиданиями приложения.

Смонтируйте приложение под /MyApp с помощью DispatcherMiddleware.

from werkzeug.middleware.dispatcher import DispatcherMiddleware as Mux

# оставьте экземпляр Flask, определённый ранее, под именем `svc`
application = Mux(None, {
    '/MyApp': svc
})

После этого приложение явно обслуживается по пути /MyApp. Промежуточное ПО ProxyFix больше не требуется.

Обновите правило переписывания IIS так, чтобы подпуть сохранялся, и убедитесь, что IIS не перехватывает расширения статических файлов, которые должен обслуживать 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>

Наконец, запустите waitress и укажите WSGI-точку входа, в которой подключено смонтированное приложение.

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

Почему это важно

Когда приложение разворачивается под подпутём за обратным прокси, прокси и приложение должны договориться о префиксе. Если прокси удаляет префикс, а приложение генерирует URL с ним, разрешение путей к ресурсам ломается. Согласование префикса на стороне прокси и на уровне WSGI предотвращает 404 для статики и делает маршрутизацию предсказуемой.

Выводы

Если локально статика работает, а за IIS — нет, проверьте, не отбрасывается ли сегмент виртуального каталога. Смонтируйте приложение Flask под тем же подпутём через DispatcherMiddleware и скорректируйте правило URL Rewrite, чтобы этот сегмент проходил дальше. Если IIS перехватывает расширения статических файлов, удалите эти сопоставления — тогда бэкенд сможет отдавать ресурсы. После этих правок шаблоны, css, js и изображения стабильно разрешаются внутри виртуального каталога.

Статья основана на вопросе на Stack Overflow от Juan Calvo Franco и ответе от Juan Calvo Franco.