RewriteRule を .htaccess に記述する際の罠

先日、ちょっとやりたいことがあり、 mod_rewrite を使ったのですが、うまく動かなくてapache の mod_rewrite のドキュメントを確認したところ、意外な罠があったのでそのメモです。

私がやろうとしたのは以下のようなものです。

  • myapp ディレクトリのなかに .htaccess と test.py がある
  • http://www.example.com/myapp/q/param へのアクセスを http://www.example.com/myapp/search.py/param と書き換えて、 search.py に PATH_INFO として "/param" が渡るようにする

最初に書いた失敗例の .htaccess はこんな感じです。

RewriteEngine on
RewriteRule ^q/ search.py/$1 [L]

で、結局正しい記述はこう。

RewriteEngine on
RewriteBase /myapp/
RewriteRule ^q/ search.py/$1 [L]

あ、それは知ってる、って人もいるとは思いますが、これ、PATH_INFO じゃなくて、パラメータとして渡す場合は、RewriteBaseを書いていなくても大丈夫だったりと、意外と複雑なトリックが隠されています。

.htaccess では RewriteRule の挙動が変わる

これはドキュメントをちゃんと読むと書いてあるのですが、RewriteRule のパターンマッチする相手は、RewriteRule を記述したコンテキストによって変わり、.htaccess 内(コンテキスト)では、URLではなく、ファイルシステム上のパスでパターンマッチすることになります。(通常は URL-path なので、以下に書いたような面倒な話は起こりません。)

その際、そのディレクトリまでのパスは自動的に削られて、RewriteRule が適用され、rewriteされたあとは、削られたパスが再度自動でつけなおされます

例をあげて説明しましょう。サーバーの設定が以下のようになっていたとして、

DocumentRoot "/var/www/example.com"
AliasMatch "^/myapp" "/opt/myapp-1.2.3"

/opt/myapp-1.2.3/.htaccess の中身が

RewriteEngine On
RewriteRule "^index\.html$"  "welcome.html"

こうなっていたとします。

ここで、http://www.example.com/myapp/index.html にアクセスがあった場合、

  • Alias により、 /opt/myapp-1.2.3/index.html へのアクセスと解釈される
  • /opt/myapp-1.2.3/.htaccess が適用される
  • .htaccess が置いてある /opt/myapp-1.2.3/ ディレクトリまでの prefix が削除された "index.html" 部分が、RewriteRule の比較対象となる
  • 比較に適合するので、 "welcome.html" と書き換えられる
  • prefix がつけなおされて /opt/myapp-1.2.3/welcome.html が最終的な書き換え結果となる

ここで、さらにトリックがあり、書き換え後のパスは URL-path なのか filesystem のパスなのか自動的に判定され、それらしい方で評価されます

判定の方法は、パスの先頭のディレクトリ(この例の場合 opt) が filesystem 上に存在するかどうか、らしいので、この例の場合は filesystem のパスとして認識され、 welcome.html にアクセスされることになります。

RewriteBase の効果

上記の例では、RewriteBaseがかかれていなくても動作してしまいました。では、もし、.htaccess に RewriteBase がかかれていた場合どうなるでしょうか。

RewriteEngine On
RewriteBase /myapp/
RewriteRule "^index\.html$"  "welcome.html"

RewriteBase は、prefix をつけ直す際に、削除した prefix の代わりに付ける prefix を指定するものです。ですから、前述の例では、

  • http://www.example.com/myapp/index.html へのアクセス
  • /opt/myapp-1.2.3/index.html (Alias によるファイルシステムへのマッピング)
  • index.html (prefix が削られた RewriteRule のマッチング対象)
  • welcome.html (rewrite 結果)
  • /myapp/welcome.html (prefix のつけなおし ← ここが変わる)

となります。
ファイルシステム上に /myapp というディレクトリは多分無いでしょうから、これは URL-path としてみなされ、再度 alias が適用されて /opt/myapp-1.2.3/welcome.html へのアクセスということになり、結果は同じになります。

RewriteBase が必要な理由

それでは、RewriteBaseが必要だった最初の例に戻ってみます。

RewriteEngine on
RewriteRule ^q/ search.py/$1 [L]

ここで http://www.example.com/myapp/q/param にアクセスがあると……

  • /myapp/q/param (URL-path)
  • /opt/myapp-1.2.3/q/param (Alias によるファイルシステムへのマッピング)
  • q/param (prefix が削られた RewriteRule のマッチング対象)
  • search.py/param (rewrite 結果)
  • /opt/myapp-1.2.3/search.py/param (prefix のつけなおし)

こうなります。 前述の通り、最終的なパスは URL-path かファイルシステムのパスかの自動判別が行われ、この場合ファイルシステムのパスということになりますので、このパスの通りのファイル名のファイルを探しに行きますが、 param なんてファイルは存在しないので "404 Not Found" になります。

「あれ? search.py があるから、それがCGIとして起こされるんじゃないの?」と思われるかもしれませんが、それは、URL-path からファイルシステムマッピングする際に行われる処理なので、一度ファイルシステムのパスという扱いになってしまった後ではその機能は働かないようです。

RewriteBase /myapp/ の記述があれば、rewrite 結果は /myapp/search.py/param となりますので、URL-pathとなり、再度 Alias 評価からやりなおされて、/opt/myapp-1.2.3/search.py (PATH_INFO=/param) に処理が渡ります。


最初に書いた通り、このような事を意識しなければならないのは、コンテキストでの記述となる .htaccess に RewriteRule を書く場合のみです。
まぁ、結局は、とりあえず何も考えずに RewriteBase は付けておけ、ってことなんですけどね。(ディレクトリ名のリネームとかする際は忘れずに同期変更しないといけないので、ちょっと気後れしますが。)