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 を指定する

関連記事:

お気軽にご相談ください

お見積りへ お問い合わせへ
▲