この2行は、何を確かめているのか
WordPress を入れると、.htaccess にこう書かれます。
RewriteEngine On RewriteBase / RewriteRule ^index\.php$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule . /index.php [L]
緑の2行が今回の主題です。意味は「実在するファイルでもディレクトリでもないなら」。 裏を返せば、実在するものは書き換えずに素通しする——静的ファイルを守るための門番です。
%{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つの条件の性質を決めています。
-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] # 実在するなら何もせず打ち切り
!-f 単独では、ディレクトリも真になる
ここが今回いちばん伝えたいところです。-f が判定しているのは
「通常ファイルか」だけです。ディレクトリは通常ファイルではないので
-f は偽、したがって !-f は真になります。
| リクエスト | 実体 | -f | !-f |
|---|---|---|---|
| /logo.png | ファイル | 真 | 偽 |
| /wp-admin/ | ディレクトリ | 偽 | 真 ← |
| /2026/07/my-post/ | 存在しない | 偽 | 真 |
つまり !-f 単独では、存在しないパスと、実在するディレクトリが同じ扱いになります。
「ファイルが無いなら」と書いたつもりが、「ファイルが無いなら(ディレクトリでも)」になってしまう。
!-d を併記します。
2行そろえて初めて「ファイルでもディレクトリでもない」を表せます。
!-d はディレクトリを除外するためだけに存在している、と覚えておくと忘れません。
!-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 へ書き換えられてしまう。
判定を、並べて見る
同じファイル構成に、2つの条件セットを当ててみます。 差が出る行が赤くなります。
■ wp-admin/ └ □ index.php ■ js/ └ □ common.js
■ wp-content/ └ ■ uploads/ └ □ photo.jpg
!-f だけの側では、/wp-admin/ と /wp-content/ が
index.php へ書き換えられています。
静的ファイルは両方とも正しく素通しされるので、画像やCSSを見ているだけでは異常に気づけません。
壊れるのはディレクトリへのアクセスだけです。
実在チェックを忘れて、500 を出した
うちで実際に起きたことです。WordPress マルチサイト用の .htaccess に、こんな行がありました。
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 は、いちばん最初にこの行を置いているのです。
RewriteRule ^index\.php$ - [L] # index.php への要求は、何もせず(-)ここで打ち切る(L) # 以降のルールに触らせないための「除外」
-f で実在しないパスに発火させない
② パターンが入力と同じ結果を返さないようにする(除外行や、量指定子を ? から + へ)。
どちらか片方では防ぎきれません。
知っておくと役に立つこと
AH01071 の対策にも使う
Primary script unknown は、実在しない .php を
PHP-FPM に渡してしまうのが原因です。渡す前に実在を確かめれば止まります。
<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 がすべてを受けてしまうためです。
「無い」の意味を、正確に書く
!-f は「ファイルが無い」ではなく、正確には
「通常ファイルではない」です。ディレクトリはこれに含まれます。
日本語で考えると同じに思える2つが、Apache では別物でした。
条件文は、書いたつもりの意味ではなく、書いた通りの意味で動きます。
この記事のまとめ
%{REQUEST_FILENAME}は URL ではなくファイルシステム上のパス。実際にディスクを見る。-f=通常ファイル、-d=ディレクトリ。両方が真になることはないが、両方偽は普通に起きる。!-f単独ではディレクトリも真になる。だから!-dを併記する。!-dを省くと mod_dir に届かず、DirectoryIndex も末尾スラッシュ補完も失われる。- 連続する
RewriteCondは AND。OR にするには[OR]が必要。 - ループ防止には 実在チェックと、入力と同じ結果を返さないパターンの両方が要る。
<If>の中では-f %{REQUEST_FILENAME}と前置になる。