2026年5月22日
2026年5月22日
WordPress REST APIが動作しない問題の解決|コード側の原因と正しい書き方
WordPressのREST API(/wp-json/)が動かない原因は、大きく「サーバーの設定(パーマリンク・リライト・Authorizationヘッダー)」と「テーマやプラグインのコード」に分かれます。この記事では、後者のコード側の原因を中心に、よくある誤りと正しい書き方を説明します。サーバー側の原因(/wp-json/ が404になる、アプリケーションパスワードで認証しても401になるなど)は、WordPress REST APIが動かないときの対処法で詳しく扱っています。
コード例は、2026年10月時点の WordPress 7.1 と PHP 8.3 で動く書き方です。
まず症状を確認する
example.com は自分のドメインに置き換えます。
# REST API 全体が動いているか(-i でステータスコードも表示)
curl -i https://example.com/wp-json/
# パーマリンクの設定に関係なく使える形式
curl -i "https://example.com/?rest_route=/wp/v2/posts&per_page=1"
| 症状 | 主な原因 | 見る章 |
|---|---|---|
どのエンドポイントも401・403になり、エラーの code が rest_ で始まる |
テーマやプラグインのコードが REST API を止めている | REST APIを止めるコードを直す |
特定の投稿タイプのURLだけ rest_no_route |
show_in_rest が有効になっていない |
カスタム投稿タイプ・タクソノミー・メタを出す |
| 管理画面やフロントのJavaScriptから呼ぶと、ログイン中なのに401・403 | ノンス(X-WP-Nonce)を送っていない、または古い |
JavaScriptから呼ぶときのノンス |
独自のエンドポイントが rest_no_route |
登録のタイミングや名前空間・メソッドの不一致 | 独自エンドポイントを正しく登録する |
WP-CLI が使える場合は、登録されているルートの一覧を確認できます。
wp eval 'foreach ( array_keys( rest_get_server()->get_routes() ) as $route ) { echo $route, PHP_EOL; }'
REST APIを止めるコードを直す
すべてを止めてしまう例
次のように、rest_authentication_errors フィルターで常にエラーを返すと、REST API はすべて使えなくなります。ブロックエディターも REST API を使うので、投稿の保存ができなくなります。
add_filter( 'rest_authentication_errors', fn() => new WP_Error( 'rest_disabled', 'REST API disabled' ) );
テーマやプラグインの中に、このようなコードがないかを検索します。
grep -rn "rest_authentication_errors" wp-content/themes wp-content/plugins wp-content/mu-plugins
効かない制限の例
ログインしていない人だけを止めるつもりで、次のように書かれた例があります。
// 誤り:この条件は成り立たないので、何も制限されない
add_filter( 'rest_authentication_errors', function ( $result ) {
if ( ! is_user_logged_in() && ! str_starts_with( rest_get_url_prefix(), 'wp-json' ) ) {
return new WP_Error( 'rest_not_logged_in', '認証が必要です', array( 'status' => 401 ) );
}
return $result;
} );
rest_get_url_prefix() は、リクエストされたURLではなく、REST API のURLの接頭辞(初期値は wp-json)を返す関数です。rest_url_prefix フィルターで変えていなければ常に wp-json なので、! str_starts_with( ... ) は常に偽になり、この制限は何もしません。
ログインしていない人だけを止める書き方
開発者向けハンドブック(REST API Handbook)のFAQで紹介されている形は次のとおりです。ほかの処理がすでに認証の結果を決めている場合は、その結果をそのまま返します。
<?php
add_filter( 'rest_authentication_errors', function ( $result ) {
if ( true === $result || is_wp_error( $result ) ) {
return $result;
}
if ( ! is_user_logged_in() ) {
return new WP_Error( 'rest_not_logged_in', 'ログインが必要です。', array( 'status' => 401 ) );
}
return $result;
} );
この形では、問い合わせフォームのプラグインなど、ログインしていない訪問者が REST API を使う機能も止まります。一部のルートだけを許可したい場合は、ルートを受け取れる rest_pre_dispatch フィルターで判定します。
<?php
add_filter( 'rest_pre_dispatch', function ( $result, $server, $request ) {
if ( ! empty( $result ) || is_user_logged_in() ) {
return $result;
}
// ログインしていなくても使わせるルートの先頭部分(例)
$allowed = array( '/contact-form-7/v1/' );
foreach ( $allowed as $prefix ) {
if ( str_starts_with( $request->get_route(), $prefix ) ) {
return $result;
}
}
return new WP_Error( 'rest_not_logged_in', 'ログインが必要です。', array( 'status' => 401 ) );
}, 10, 3 );
$allowed には、上のWP-CLIのコマンドで確認した、実際に使うルートを入れます。
カスタム投稿タイプ・タクソノミー・メタを出す
カスタム投稿タイプ
register_post_type() で show_in_rest を true にしないと、その投稿タイプは REST API にもブロックエディターにも出てきません。
<?php
add_action( 'init', function () {
register_post_type( 'product', array(
'label' => '商品',
'public' => true,
'show_in_rest' => true, // REST API とブロックエディターに対応させる
'rest_base' => 'products', // /wp-json/wp/v2/products になる
'supports' => array( 'title', 'editor', 'thumbnail', 'custom-fields' ),
) );
// カスタムタクソノミーも show_in_rest が必要(ないとエディターの画面に出ない)
register_taxonomy( 'product_cat', 'product', array(
'label' => '商品カテゴリー',
'hierarchical' => true,
'show_in_rest' => true,
) );
} );
登録した後は、curl https://example.com/wp-json/wp/v2/products で一覧が返るかを確認します。プラグインが登録した投稿タイプの場合は、そのプラグインの設定に REST API の項目がないかを確認します。
カスタムフィールド(メタ)
投稿のメタは、register_post_meta() で show_in_rest を指定したものだけが、レスポンスの meta に出ます。投稿タイプの supports に custom-fields が必要です。
<?php
add_action( 'init', function () {
register_post_meta( 'product', 'price', array(
'show_in_rest' => true,
'single' => true,
'type' => 'number',
'auth_callback' => function () {
return current_user_can( 'edit_posts' );
},
) );
} );
auth_callback は、REST API などからこのメタを書き換えてよいかの判定です。読み取りだけなら、公開されている投稿のメタは誰でも見られます。
レスポンスに独自の項目を足す
メタ以外の値(計算した値など)をレスポンスに加えたい場合は、rest_prepare_post フィルターで data を直接書き換えるより、register_rest_field() を使います。スキーマに載るのでエンドポイントの説明(OPTIONS リクエスト)にも出るほか、_fields パラメーターで項目を絞ったときは、その項目が指定された場合だけ値が計算されます。
<?php
add_action( 'rest_api_init', function () {
register_rest_field( 'post', 'reading_minutes', array(
'get_callback' => function ( $post ) {
$chars = mb_strlen( wp_strip_all_tags( get_post_field( 'post_content', $post['id'] ) ) );
return max( 1, (int) ceil( $chars / 600 ) ); // 1分に600字として計算
},
'schema' => array(
'description' => '読了までの目安(分)',
'type' => 'integer',
'context' => array( 'view', 'edit' ),
'readonly' => true,
),
) );
} );
JavaScriptから呼ぶときのノンス
ログインしているユーザーのブラウザから fetch() などで REST API を呼ぶ場合(Cookie認証)、WordPress はリクエストにノンス(wp_rest というアクション名で作ったもの)が付いているかを確認します。
- ノンスを付けないと、ログイン中でも「ログインしていないユーザー」として扱われ、権限が必要な操作は401などになります
- 古いノンスや誤ったノンスを付けると、
rest_cookie_invalid_nonce(403)になります
PHP側でノンスとURLをスクリプトに渡し、X-WP-Nonce ヘッダーで送ります。my-script はスクリプトのハンドル名の例です。
<?php
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_script( 'my-script', get_theme_file_uri( 'js/my-script.js' ), array(), '1.0', true );
wp_add_inline_script(
'my-script',
'const myApi = ' . wp_json_encode( array(
'root' => esc_url_raw( rest_url() ),
'nonce' => wp_create_nonce( 'wp_rest' ),
) ) . ';',
'before'
);
} );
// js/my-script.js
fetch( myApi.root + 'wp/v2/users/me', {
headers: { 'X-WP-Nonce': myApi.nonce },
credentials: 'same-origin',
} )
.then( ( res ) => res.json() )
.then( ( user ) => console.log( user.name ) );
ブロックエディター用のスクリプトで @wordpress/api-fetch(wp.apiFetch)を使う場合は、ノンスとURLが自動で設定されるので、この処理は要りません。
ページがキャッシュされていると、HTMLに埋め込んだノンスが古くなり、rest_cookie_invalid_nonce になることがあります。ログインユーザーにキャッシュを返していないかを確認してください。
外部のプログラムやサーバーから呼ぶ場合は、Cookie認証ではなく、アプリケーションパスワード(「ユーザー」→「プロフィール」で発行)を使います。
独自エンドポイントを正しく登録する
独自のエンドポイントは、rest_api_init アクションの中で register_rest_route() を呼んで登録します。init など別のタイミングで呼ぶと正しく登録されません。permission_callback は省略できず、省略すると WordPress が注意(_doing_it_wrong)を出します。
<?php
add_action( 'rest_api_init', function () {
register_rest_route( 'myplugin/v1', '/items/(?P<id>\d+)', array(
'methods' => WP_REST_Server::READABLE, // GET
'callback' => 'myplugin_rest_get_item',
'permission_callback' => function () {
return current_user_can( 'read' );
},
'args' => array(
'id' => array(
'validate_callback' => function ( $value ) {
return is_numeric( $value );
},
'sanitize_callback' => 'absint',
'required' => true,
),
),
) );
} );
function myplugin_rest_get_item( WP_REST_Request $request ) {
$post = get_post( $request->get_param( 'id' ) );
if ( ! $post instanceof WP_Post ) {
return new WP_Error( 'myplugin_not_found', '見つかりません。', array( 'status' => 404 ) );
}
return rest_ensure_response( array(
'id' => $post->ID,
'title' => get_the_title( $post ),
) );
}
この例のURLは https://example.com/wp-json/myplugin/v1/items/123 です。rest_no_route になる場合は、名前空間(myplugin/v1)、ルート(/items/123)、HTTPメソッド(GETかPOSTか)が登録内容と一致しているかを確認します。誰でも読める公開データを返すときだけ、permission_callback に '__return_true' を指定します。
まとめ
rest_get_url_prefix()で判定する制限は効かない。ログインしていない人だけを止める場合は、すでに決まった結果を尊重する形でrest_authentication_errorsを書く。一部だけ許可するならrest_pre_dispatchを使う- カスタム投稿タイプとタクソノミーは
show_in_rest、メタはregister_post_meta()のshow_in_rest(とcustom-fieldsのサポート)が必要 - レスポンスへの項目の追加は
register_rest_field()を使う - ブラウザから呼ぶときは
wp_restのノンスをX-WP-Nonceで送る。外部からはアプリケーションパスワードを使う - 独自エンドポイントは
rest_api_initの中で登録し、permission_callbackを指定する
関連記事: