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

APPW.jp
 
WordPress REST API mu-plugins Android iOS Apache

WordPress REST APIが403になった原因はmu-pluginsだった
― Android / iOS アプリ接続トラブルの全解決記録

5サイト共通のアクセスログ出力関数を mu-plugins に移行したところ、 WordPress の Android / iOS アプリがサイトに接続できなくなった。 ヘッダー1行から原因を絞り込み、3つの修正で完全解決するまでの実録。

// 01 — BACKGROUND

背景:mu-plugins に移行した独自ロガー

運営する5サイトでは、共通のオリジナル PHP 関数 uah_logging() を使ってアクセスログを出力している。 この関数は /var/www/html/templates/log.php に定義されており、 各サイトの WordPress テーマから呼び出していた。

保守性向上のため、呼び出し処理を mu-plugins/access-logger.php に集約した。 mu-plugins はプラグインの有効化操作なしに常時実行されるため、サイト横断の共通処理に都合がよい。 しかし、この移行後に WordPress の Android / iOS アプリが突然サイト情報を取得できなくなった。

⚠ TRIGGER
mu-plugins に移行した直後から「アプリケーションパスワードに対応していない」エラーが発生。 ブラウザからはアプリケーションパスワードの作成が可能なため、サーバー設定に問題があると判断した。

// 02 — DIAGNOSIS

切り分けの流れ

まず REST API エンドポイントに直接アクセスして状態を確認する。

shell — REST API 応答確認
$ curl -I https://example.com/wp-json/wp/v2/
HTTP/2 403
vary: accept,content-type
set-cookie: PHPSESSID=375a7a7c...; path=/
content-type: text/html; charset=UTF-8
server: Apache/2.4.58 (Ubuntu)

▸ ヘッダーを読む

このレスポンスには重要な手がかりが含まれている。 PHPSESSID が返っているということは、リクエストが Apache を通過し PHP まで到達している証拠だ。 Apache が遮断しているならセッション Cookie は絶対に返らない。 つまり 403 を返しているのは Apache ではなく WordPress(PHP)の内側である。

レスポンスヘッダー 意味 判断
HTTP/2 403 アクセス拒否 ✗ 問題あり
PHPSESSID=... PHP まで到達済み → Apache は無関係
content-type: text/html JSON ではなく HTML を返している ✗ WordPress が処理していない

▸ プラグイン全無効化テスト

shell — plugins フォルダをリネームして全無効化
$ mv /var/www/html/wp-content/plugins \
       /var/www/html/wp-content/plugins_disabled

$ curl -I https://example.com/wp-json/wp/v2/
HTTP/2 403  # ← 変わらない → plugins は無関係

通常のプラグインを全無効化しても 403 が継続。 mu-pluginsplugins フォルダのリネームでは無効化されないため、 次の容疑者として mu-plugins を確認する。

shell — mu-plugins をリネームして無効化
$ mv /var/www/html/wp-content/mu-plugins \
       /var/www/html/wp-content/mu-plugins-d

$ curl -I https://example.com/wp-json/wp/v2/
HTTP/2 200  # ← 200 に変わった → 犯人確定
link: <https://example.com/index.php?rest_route=/>; rel="https://api.w.org/"
/wp-json/wp/v2/ → 403  PHPSESSID あり → PHP到達済み
Apache設定・mod_security・.htaccess を確認 → 異常なし
plugins 全無効化 → 403 継続
mu-plugins リネーム → 200 に変化
mu-plugins/access-logger.php が原因と確定

// 03 — FIX 1

修正①:access-logger.php に REST API スキップを追加

access-logger.phptemplate_redirect フックから log.php を 呼び出すシンプルな構造だった。log.php 内の処理が REST API リクエストに対して 403 を返していたため、 REST API リクエスト時はロガーをスキップする条件を追加する。

php — mu-plugins/access-logger.php(修正後)
<?php
/**
 * Plugin Name: Access Logger
 */

// PATH_INFO 方式の REST リクエスト対応(wordpress-rs / iOS アプリ用)
add_action('parse_request', function(&$wp) {
    $path_info = $_SERVER['PATH_INFO'] ?? '';
    if ($path_info
        && strlen($path_info) > 1
        && isset($wp->query_vars['rest_route'])
        && $wp->query_vars['rest_route'] === '/') {
        $wp->query_vars['rest_route'] = $path_info;
    }
}, 1); // priority 1 = rest_api_loaded (priority 10) より前に実行

add_action('template_redirect', function() {
    my_access_log();
});

function my_access_log() {
    // REST API リクエストはスキップ(WordPress 定数チェック)
    if (defined('REST_REQUEST') && REST_REQUEST) {
        return;
    }
    // フォールバック:URL パスで直接チェック
    if (strpos($_SERVER['REQUEST_URI'], '/wp-json/') !== false) {
        return;
    }
    // PATH_INFO 方式の REST リクエストもスキップ
    if (!empty($_SERVER['PATH_INFO'])) {
        return;
    }

    $templates_path = "/var/www/html/";
    $cnpath         = $templates_path . "templates/";
    $lgpath         = $cnpath . "log.php";

    include_once $lgpath;
    if (function_exists('uah_logging')):
        $uqid = uah_logging();
    endif;
}
ℹ NOTE — REST_REQUEST 定数について
WordPress は REST API リクエスト処理中に REST_REQUEST 定数を true に設定する。 これが最も確実なチェック方法だが、parse_request フックより後に定義されるため、 フォールバックとして URI パターンチェックも併用している。

// 04 — FIX 2

修正②:.htaccess に wp-json リライトルールを追加

mu-plugins を元に戻して Android アプリから再登録を試みたが「アプリケーションパスワード非対応」エラーが継続した。 改めて /wp-json/index.php?rest_route=/ を比較確認する。

shell — エンドポイント別の応答比較
# /wp-json/ 経由 → ボディが空
$ curl -s https://example.com/wp-json/ | python3 -m json.tool | grep -A 5 "authentication"
Expecting value: line 1 column 1 (char 0)

# index.php?rest_route=/ 経由 → JSON が返る
$ curl -s "https://example.com/index.php?rest_route=/" | python3 -m json.tool | grep -A 5 "authentication"
    "authentication": {
        "application-passwords": {
            "endpoints": {
                "authorization": "https://example.com/wp-admin/authorize-application.php"

/wp-json/ へのアクセスが空レスポンスを返す原因は、 .htaccess の wp-json リライトルールが欠落していたためだ。 パーマリンク設定の再保存などで .htaccess が上書きされた際に wp-json のルールが消えることがある。

apache — .htaccess 追記内容
# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^wp-json/(.*) index.php?rest_route=/$1 [QSA,L]  ← この行を追加
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
# END WordPress
⚠ CAUTION
WordPress の管理画面から「設定 → パーマリンク → 変更を保存」を実行すると .htaccess が自動再生成される。 その際 wp-json のリライトルールが消える場合があるため、保存後は必ず確認すること。

// 05 — FIX 3

修正③:iOS アプリ固有の PATH_INFO 問題

Android アプリは正常に動作するようになったが、iOS アプリ(iPad)では「サイトの情報が取得できません」エラーが継続した。 アクセスログを確認すると原因が明確になった。

access.log — iOS アプリのリクエスト
"POST /index.php?rest_route=/wp/v2/users/me/application-passwords..." 201 988
"POST /xmlrpc.php" 200 777
"GET /index.php/wp/v2/users/me?rest_route=/&context=edit" 200 600113
"GET /index.php/wp/v2/settings?rest_route=/&context=view" 200 599739
"GET /index.php/jetpack/v4/site?rest_route=/" 200 600113

▸ 問題の構造

iOS アプリが使用する wordpress-rs ライブラリは、REST エンドポイントを PATH_INFO 方式でリクエストする。

— Android vs iOS のリクエスト方式の違い
# Android(クエリパラメータ方式)
GET /index.php?rest_route=/wp/v2/users/me&context=edit
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^
                WordPress が正しく読み取る

# iOS / wordpress-rs(PATH_INFO 方式)
GET /index.php/wp/v2/users/me?rest_route=/&context=edit
               ^^^^^^^^^^^^^^^  ^^^^^^^^^^^
               PATH_INFO        クエリパラメータ(rest_route=/)

# WordPress は PATH_INFO を無視して rest_route=/ のみ読む
# → ルートスキーマ全体(≈600KB)が返る
Android アプリ iOS アプリ(wordpress-rs)
リクエスト方式 クエリパラメータ PATH_INFO
エンドポイント例 ?rest_route=/wp/v2/users/me /index.php/wp/v2/users/me?rest_route=/
WordPress の解釈 ✓ 正しく解釈 ✗ rest_route=/ のみ読む
レスポンス ユーザー情報 JSON ルートスキーマ 600KB

▸ 対策:parse_request で PATH_INFO を優先

parse_request フック(priority 1)で PATH_INFO が存在する場合に rest_route クエリ変数を上書きする。 この処理は修正①の access-logger.php に同時に組み込み済みである。

shell — 修正後の動作確認
# PATH_INFO 方式で直接テスト(未認証 → 401 が正常)
$ curl -s "https://example.com/index.php/wp/v2/users/me?rest_route=/&context=edit" \
  | python3 -m json.tool | head -10
{
    "code": "rest_not_logged_in",
    "message": "現在ログインしていません。",
    "data": { "status": 401 }
}

# 修正前:ルートスキーマ 600KB が返っていた
# 修正後:正しいエンドポイントに到達(401 = 認証待ち = 正常)
ℹ NOTE
401 rest_not_logged_in は正常な動作。iOS アプリはアプリケーションパスワードを Authorization ヘッダーに付けてリクエストするため、認証が通って 200 が返る。

// 06 — SUMMARY

修正の全体像

# 問題 原因 対策
REST API が 403 mu-plugins の access-logger.php が include する log.php が遮断 REST_REQUEST / PATH_INFO をスキップする条件を追加
アプリが「パスワード非対応」エラー .htaccess の wp-json リライトルール欠落 ^wp-json/(.*) → index.php?rest_route=/$1 を追記
iOS アプリのみ情報取得エラー wordpress-rs が PATH_INFO 方式でリクエスト、WordPress が無視 parse_request で PATH_INFO を rest_route に反映

// LESSONS LEARNED

  • mu-plugins は plugins フォルダの無効化でも常に実行される。REST API への影響を必ず考慮すること。
  • PHPSESSID の有無でApache遮断か PHP内部エラーかを即座に切り分けられる。
  • レスポンスサイズが 600KB なら「誤ったエンドポイント」のサイン。中身より先にサイズを見る。
  • Android(クエリパラメータ)と iOS / wordpress-rs(PATH_INFO)でリクエスト方式が異なる。
  • パーマリンク設定を保存し直すと wp-json リライトルールが消えることがある。
  • mu-plugins に REST API に関係する処理を加えるときは、最初から REST_REQUEST チェックを入れる習慣を。

『WordPress REST APIが403になった原因はmu-pluginsだった』を公開しました。