スマートフォン・タブレットからインターネットサーバーオペレーション

APPW.jp
 
mod_rewrite / file existence test

実在するか、を確かめる
RewriteCond の -f と -d

WordPress の .htaccess に必ず出てくる2行。なんとなくコピーして使っていましたが、 !-f だけではディレクトリも真になることを知らないまま書くと、 分かりにくい壊れ方をします。判定の意味と、実在チェックを忘れて500エラーを出した実例です。

Apache 2.4 mod_rewrite REQUEST_FILENAME WordPress

[ § 0 ] 導入

この2行は、何を確かめているのか

WordPress を入れると、.htaccess にこう書かれます。

.htaccess — WordPress の定番
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]

緑の2行が今回の主題です。意味は「実在するファイルでもディレクトリでもないなら」。 裏を返せば、実在するものは書き換えずに素通しする——静的ファイルを守るための門番です。

[ § 1 ] 前提

%{REQUEST_FILENAME} は URL ではない

まずここを押さえておくと、後がすべて素直に読めます。 %{REQUEST_FILENAME} が持っているのは URL ではなく、 ファイルシステム上のパスです。

対応関係
# リクエスト
https://appw.jp/wp-content/uploads/photo.jpg

# %{REQUEST_FILENAME} の中身
/home/webm/www/appw.jp/wp-content/uploads/photo.jpg

だから -f-d は、実際にディスクを見に行きます。 URL のパターンを見ているのではなく、そこに本当にファイルがあるかを確かめている。 これがこの2つの条件の性質を決めています。

[ § 2 ] 基本

-f と -d の意味

条件真になるとき
-f実在する通常ファイルである
-d実在するディレクトリである
!-f実在する通常ファイルではない
!-d実在するディレクトリではない

-f-d は互いに排他的です。ファイルであってディレクトリでもある、 ということはないので、両方が真になることはありません。 逆に両方が偽になることは頻繁に起きます——存在しないパスがそれです。

条件は AND でつながる

連続する RewriteCond は、暗黙の AND です。 OR にしたいときだけ [OR] を付けます。

連結のしかた
# AND:ファイルでもディレクトリでもない
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d

# OR:ファイルかディレクトリのどちらかである(=実在する)
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]        # 実在するなら何もせず打ち切り
[ § 3 ] 落とし穴

!-f 単独では、ディレクトリも真になる

ここが今回いちばん伝えたいところです。-f が判定しているのは 「通常ファイルか」だけです。ディレクトリは通常ファイルではないので -f、したがって !-fになります。

リクエスト実体-f!-f
/logo.pngファイル
/wp-admin/ディレクトリ真 ←
/2026/07/my-post/存在しない

つまり !-f 単独では、存在しないパスと、実在するディレクトリが同じ扱いになります。 「ファイルが無いなら」と書いたつもりが、「ファイルが無いなら(ディレクトリでも)」になってしまう。

だから !-d を併記します。 2行そろえて初めて「ファイルでもディレクトリでもない」を表せます。 !-dディレクトリを除外するためだけに存在している、と覚えておくと忘れません。
[ § 4 ] 影響

!-d を省くと、何が壊れるか

実在するディレクトリへのアクセスが index.php に飲み込まれます。 すると、そのリクエストは mod_dir に到達しなくなります。 mod_dir が担当していた仕事が、まとめて失われるわけです。

失われるもの結果
DirectoryIndexそのディレクトリの index.php / index.html が使われない
末尾スラッシュの補完/dir/dir/ の 301 が起きない
ディレクトリ一覧Options +Indexes でも出ない

WordPress なら、/wp-admin/ が管理画面ではなくフロントエンドのルーティングに回されます。 本来なら wp-admin/index.php が DirectoryIndex として選ばれるはずが、 その前に /index.php へ書き換えられてしまう。

この壊れ方は原因が見えにくい。 エラーログには何も出ません。500 でも 404 でもなく、 「なぜか管理画面がトップページになる」という挙動になります。 rewrite を疑うまでに時間がかかる類のバグです。
[ § 5 ] 体感デモ

判定を、並べて見る

同じファイル構成に、2つの条件セットを当ててみます。 差が出る行が赤くなります

Interactive · -f / -d evaluation
実在チェックの判定表
下のファイル構成に対して、各リクエストがどう判定されるかを表示します。
サーバー上のファイル構成( ディレクトリ / ファイル)
index.php  logo.png  robots.txt
wp-admin/  └ index.php  js/ └ common.js
wp-content/ └ uploads/ └ photo.jpg

!-f だけの側では、/wp-admin//wp-content/index.php へ書き換えられています。 静的ファイルは両方とも正しく素通しされるので、画像やCSSを見ているだけでは異常に気づけません。 壊れるのはディレクトリへのアクセスだけです。

[ § 6 ] 実例

実在チェックを忘れて、500 を出した

うちで実際に起きたことです。WordPress マルチサイト用の .htaccess に、こんな行がありました。

.htaccess — 500 の原因になった行
RewriteRule ^([_0-9a-zA-Z-]+/)?(.*\.php)$ $2 [L]
#            ↑ RewriteCond が1つも無い=実在を確かめていない

先頭のディレクトリ部分が省略可(?)なので、/wxfyf.php のような ディレクトリの無い .php にもマッチします。書き換え結果は入力と同一。 Apache は書き換えが起きるたびに最初から評価し直すので、同じルールに延々とマッチし続け、 内部リダイレクト10回で 500——という流れでした。 詳しくは別記事に書いています。

-f を前置すると、どう変わるか

実在チェックを足す
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule ^([_0-9a-zA-Z-]+/)+(.*\.php)$ $2 [L]

/wxfyf.php は実在しないので -f が偽。ルールは発火しません。 スキャナがランダムな名前の .php を大量に叩いてきても、ループは起きなくなります。

ただし -f だけでは足りません。 index.php は実在するので -f は真。 このルールに index.php が入ると、やはり出力が入力と同じになって自己ループします。 だから WordPress の .htaccess は、いちばん最初にこの行を置いているのです。
1行目の意味
RewriteRule ^index\.php$ - [L]
#  index.php への要求は、何もせず(-)ここで打ち切る(L)
#  以降のルールに触らせないための「除外」
ループを防ぐには、2つの見張りが要ります。-f実在しないパスに発火させない  ② パターンが入力と同じ結果を返さないようにする(除外行や、量指定子を ? から + へ)。 どちらか片方では防ぎきれません。
[ § 7 ] 応用と注意

知っておくと役に立つこと

AH01071 の対策にも使う

Primary script unknown は、実在しない .php を PHP-FPM に渡してしまうのが原因です。渡す前に実在を確かめれば止まります。

vhost / .htaccess
<FilesMatch \.php$>
  <If "-f %{REQUEST_FILENAME}">
    SetHandler "proxy:unix:/run/php/php-fpm.sock|fcgi://localhost"
  </If>
</FilesMatch>
書き方が違う点に注意。 <If> の中では -f %{REQUEST_FILENAME}前置になります。 RewriteCond %{REQUEST_FILENAME} -f の後置とは順序が逆です。 同じ判定なのに構文が違うので、書き間違えやすいところです。

毎回 stat() が走る

-f-d は実際にファイルシステムを触ります。 リクエストごと、条件ごとに stat() が呼ばれるので、 大量に並べれば効いてきます。 とはいえ通常の数行なら気にする必要はありません。順序を工夫して、 早く打ち切れるルールを前に置くほうが効果的です。

末尾スラッシュとの関係

-d が真でも、末尾スラッシュが無いリクエストには mod_dir が 301 を返して補完します。 この挙動が絡むと、存在しないパスが 301 → 200 を返す——いわゆるソフト404が起きることがあります。 catch-all の RewriteRule . /index.php がすべてを受けてしまうためです。

[ § 8 ] まとめ

「無い」の意味を、正確に書く

!-f は「ファイルが無い」ではなく、正確には 「通常ファイルではない」です。ディレクトリはこれに含まれます。 日本語で考えると同じに思える2つが、Apache では別物でした。 条件文は、書いたつもりの意味ではなく、書いた通りの意味で動きます

この記事のまとめ

  • %{REQUEST_FILENAME} は URL ではなくファイルシステム上のパス。実際にディスクを見る。
  • -f=通常ファイル、-d=ディレクトリ。両方が真になることはないが、両方偽は普通に起きる。
  • !-f 単独ではディレクトリも真になる。だから !-d を併記する。
  • !-d を省くと mod_dir に届かず、DirectoryIndex も末尾スラッシュ補完も失われる
  • 連続する RewriteCond は AND。OR にするには [OR] が必要。
  • ループ防止には 実在チェックと、入力と同じ結果を返さないパターンの両方が要る。
  • <If> の中では -f %{REQUEST_FILENAME}前置になる。

関連記事

『実在するか、を確かめる — RewriteCond の -f と -d』を公開しました。