flaskでリバースプロキシで、サブディレクトリで運用する際の注意

自作のflaskアプリを動かそうとしたのですが、サブディレクトリで運用しようとしたらハマりました。
しかも、意外と情報がない。あれー、結構ありうる問題だと思うのだけどなぁ。
というわけで、メモしておくことにします。

## 環境

* ubuntu24.04
* apache2-2.4.58
* python 3.12.3
* Flask 3.1.0
* gunicorn 23.0.0

## まずは、動かしてみる。

とりあえず、apacheの設定。

```
<VirtualHost *:80>
	ProxyPass / http://127.0.0.1:8000/
	ProxyPassReverse / http://127.0.0.1:8000/
	<Location />
		require all granted
	</Location>
</VirtualHost>
```

flaskのソースコード、実際は違うけど、仮に以下のようにします。

```python:wsgi.py
# coding: utf-8

from flask import Flask, url_for, make_response

app = Flask(__name__)

@app.route('/')
def index():
	url = url_for('hoge', name='aaa')
	body = f'''<html>
<body>
<p><a href="{url}">{url}</a></p>
</body>
'''
	return make_response(body, 200)

@app.route('/hoge/<name>')
def hoge(name):
	url = url_for('index')
	return f'''<html>
<body>
<p>name = {repr(name)}.</p>
<p><a href="{url}">{url}</a.</p>
</body>
</html>
'''

application = app

```

あとは、apacheのmod_proxyなどを有効にしたり、gunicornなどでサーバを立てたりして、動かします。
まぁ、今回はそこはポイントじゃないんで、端折ります。

## サブディレクトリでの運用、…で失敗する。

さて、ドキュメントルートは別の静的ドキュメントで置いときたい、とか、複数のflaskアプリを動かしたい、とか、flaskアプリを `http://example.com/myapp/` というようにサブディレクトリに置いときたい、と思うことがあるでしょう。いや、あったんです。

というわけで、apacheの設定を以下のように修正します。

```
<VirtualHost *:80>
	DocumentRoot /var/www/html

	ProxyPass /myapp/ http://127.0.0.1:8000/
	ProxyPassReverse /myapp/ http://127.0.0.1:8000/
	<Location /myapp/>
		require all granted
	</Location>
</VirtualHost>
```

ちょっと余談になりますが、「 `/myapp/` 」の最後の「 `/` 」は、付けた方がいいと思います。
付けないと、「 `http://example.com/myapp` 」と「 `http://example.com/myapp/` 」の両方でアクセスできてしまいます。
いや、それ自体はまぁ別にいいかもしれないけど、そこでHTMLの中で「 `./hoge.html` 」というリンクを貼った場合、前者は「 `http://example.com/hoge.html` 」へのリンク、後者は「 `http://example.com/myapp/hoge.html` 」となってしまい、ブレが発生してしまいます。

閑話休題。
上記の設定でapacheを動かすと、indexページは正しく表示されますが、 `hoge/aaa` へのリンクが正しく貼れません。
というのも、flaskの `url_for` はエントリポイント、具体的には `/` から始まるパスを返すからです。
つまり、 `url_for('hoge', name='aaa')` は「 `/hoge/aaa` 」を返すので、開くページは「 `http://example.com/hoge/aaa` 」になってしまいます。
まぁ、flaskアプリとしては、自分がリバースプロキシとして呼ばれているかどうかは知る由もないので、しかたないですね。

## 解決策、その1

まず解決策の一つとして、全てのリンクを相対パスに書き換える方法です。
と言っても、一つ一つ手で書き換えてしまうと `url_for` を使っていたメリットが無くなってしまいますので、ここは代わりとなる `relpath_for` という関数を作ってしまいましょう。

```python
from flask import request, url_for

def relpath_for(entry, **kwargs):
	parts = request.path[1:].split('/')
	return ('/'.join(['..'] * (len(parts) - 1)) if len(parts) > 1 else '.') + url_for(entry, **kwargs)
```

ご覧のとおり、パスの階層に従って頭に「 `..` 」を付け加えているだけです。
もっとも、これだと同じディレクトリにあっても `..` が加えられてしまうので、もしそれが気に入らなければもう少し工夫が必要です。

また、以下を参照に `app.jinja_env.globals['relpath_for'] = relpath_for` としておけば、テンプレート内でも使えそうです。

* [Jinjaテンプレートから自作関数を呼び出す](https://qiita.com/libra189/items/9679aaf5a34be33b0581)

## 解決策、その2

一応、こちらがflask公式の回答(?)です。

* [Proxyの背後にいることのFlaskへの指示](https://msiz07-flask-docs-ja.readthedocs.io/ja/latest/deploying/proxy_fix.html)

上記に従い、 `wsgi.py` を以下のように修正します。

```python:wsgi.py
app = Flask(__name__)
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)
```

(何となく、今回の問題だけで言えば `x_prefix=1` だけでいいかもしれませんが。)

しかし、これだけでは足りません。
上記のサイトにも、

> HTTPサーバは、本当の値をFlaskアプリケーションへ渡すためにX-Forwarded-ヘッダーを設定するべきです。

と書かれています。
そこで、apacheの設定ファイルも以下のように書き換えます。

```
<VirtualHost *:80>
	DocumentRoot /var/www/html

	ProxyPass /myapp/ http://127.0.0.1:8000/
	ProxyPassReverse /myapp/ http://127.0.0.1:8000/
	<Location /myapp>
		require all granted
		RequestHeader set X-Forwarded-Prefix /myapp/
	</Location>
</VirtualHost>
```

なお、 `RequestHeader` を使うには、apacheの `mod_headers` モジュールが必要です。
ubuntuの場合には、以下のコマンドで有効にしましょう。

```
sudo a2enmod headers
```



<!--

```python
# coding: utf-8

#from wsgiref.simple_server import demo_app as application

from flask import Flask, url_for, make_response
from werkzeug.middleware.proxy_fix import ProxyFix

app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)

@app.route('/')
def index():
	url = url_for('hoge', name='aaa')
	body = f'''<html>
<body>
<p><a href="{url}">{url}</a></p>
</body>
'''
	return make_response(body, 200)

@app.route('/hoge/<name>')
def hoge(name):
	url = url_for('index')
	return f'''<html>
<body>
<p>name = {repr(name)}.</p>
<p><a href="{url}">{url}</a.</p>
</body>
</html>
'''

application = app

```

```
        RequestHeader set X-Forwarded-Prefix /test/
```

-->