Подсистема WP REST API (ядро wp-includes/rest-api/) превращает WordPress в типизированный HTTP-сервер данных, управляемый диспетчером WP_REST_Server. В отличие от классического рендеринга шаблонов (template-loader.php), при обращении к префиксу /wp-json/ парсер WP::parse_request() обнаруживает переменную окружения rest_route и перенаправляет выполнение в метод rest_api_loaded(), полностью пропуская загрузку HTML-темы.
Внутренний конвейер WP_REST_Server::serve_request() реализует 5-фазный цикл обработки:
WP_REST_Server::check_authentication): Вызов фильтра rest_authentication_errors. Поддерживаются Cookie + Nonce (X-WP-Nonce для внутрибраузерных запросов Gutenberg), Application Passwords (RFC 7617 Basic Auth поверх HTTPS) и внешние Bearer-токены (JWT / OAuth 2.0).WP_REST_Server::match_request_to_handler): Регулярные выражения маршрутов, зарегистрированных на хуке rest_api_init, сопоставляются с URI и HTTP-методом (GET, POST, PUT, PATCH, DELETE), формируя объект WP_REST_Request.WP_REST_Request::has_valid_params & sanitize_params): До запуска бизнес-логики входные параметры проверяются по JSON Schema (type, format, enum, minimum, pattern) и функциям validate_callback / sanitize_callback.permission_callback): Обязательный предикат доступа. Если возвращается false или WP_Error, выполнение немедленно прерывается с кодом 401 Unauthorized или 403 Forbidden.rest_post_dispatch): Контроллер возвращает WP_REST_Response. Фильтр rest_post_dispatch применяет проекцию полей (_fields=id,title,slug), встраивает связанные ресурсы (_embed по спецификации HAL — Hypertext Application Language) и отправляет JSON-ответ.WP REST API следует рассматривать как строго типизированный шлюз сериализации графа сущностей WordPress в формат HAL+JSON с управляемой проекцией полей (_fields) и пакетным резолвингом связей (_embed).
[HTTP Запрос: GET /wp-json/wp/v2/posts?_fields=id,title,_links&_embed=wp:featuredmedia]
│
▼
[1. WP::parse_request() -> rest_api_loaded()]
│
▼
[2. Фильтр rest_authentication_errors]
├── Проверка X-WP-Nonce / Application Password / Bearer JWT
└── При ошибке ──► 401/403 WP_Error (Fast Exit)
│
▼
[3. WP_REST_Server::dispatch(WP_REST_Request)]
├── Match Regex: /wp/v2/posts
├── JSON Schema Validation: validate_callback -> sanitize_callback
└── Авторизация: permission_callback() === true
│
▼
[4. WP_REST_Posts_Controller::get_items()]
├── Оптимизация: skips rendering content if not in _fields!
└── Возврат WP_REST_Response
│
▼
[5. HAL Embedding & Field Filtering (rest_post_dispatch)]
├── Резолвинг _links['wp:featuredmedia'] -> _embedded
└── Срез JSON-ключей по маске _fieldsВыполнение цепочки rest_authentication_errors. Идентификация текущего пользователя через Nonce, App Password или JWT.
Регулярное сопоставление маршрута и проверка всех query/body аргументов против декларативной JSON Schema.
Исполнение permission_callback маршрута. Гарантирует защиту эндпоинта до обращения к базе данных.
Вызов метода контроллера. Анализ параметра _fields через rest_is_field_included() для пропуска тяжелых вычислений.
Встраивание связанных объектов (_embed), добавление HTTP-заголовков пагинации (X-WP-Total) и отправка JSON.
Клиент явно перечисляет требуемые ключи JSON (например, _fields=id,title,slug,reading_metrics,_links,_embedded).
Отсекает 80% лишнего трафика и отключает вычисление неиспользуемых полей на сервере.
Ядро автоматически разрешает связанные объекты медиафайлов, авторов и таксономий внутри массива _embedded.
Полностью устраняет каскад N+1 сетевых HTTP-запросов со стороны клиента.
Закрытие уязвимых маршрутов (/wp/v2/users) для гостей и проверка криптографических токенов доступа.
Защищает систему от User Enumeration и несанкционированного парсинга.
Считывание метаданных пагинации из HTTP-заголовков ответа X-WP-Total и X-WP-TotalPages.
Позволяет строить навигацию по страницам без дополнительных запросов подсчета.
Кэширование публичных GET-ответов на узлах CDN с мгновенной отдачей за 10–15 мс по всему миру.
Авторизованные запросы автоматически обходят кэш благодаря заголовку Cache-Control: private.
Почему запрос ?_fields=id,title,slug не только сокращает объем передаваемого по сети JSON, но и ускоряет работу самого сервера PHP?
_fields и rest_is_field_included(): При запросе /wp-json/wp/v2/posts?_fields=id,title,slug размер ответа для 20 статей падает со 180 КБ до 3.2 КБ, а сервер пропускает выполнение сотен фильтров the_content и рендеринг блоков Gutenberg._embed: Передача параметра ?_embed=author,wp:featuredmedia,wp:term позволяет фронтенду получить статьи вместе с аватарками авторов, URL обложек и категориями за 1 сетевой раунд вместо 4 последовательных запросов.rest_preload_api_request): Серверная предзагрузка начального состояния REST-эндпоинтов в глобальный кэш wp.apiFetch.createPreloadingMiddleware, благодаря чему React-приложение рендерится мгновенно без спиннеров загрузки./wp/v2/users (предотвращение перечисления логинов администраторов — User Enumeration) и добавление строгих заголовков Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400 для Edge CDN.Сравним архитектурные характеристики нативного WP REST API и WPGraphQL при проектировании Headless-инфраструктуры:
rest_is_field_included()#Начиная с современных версий ядра, при регистрации кастомного поля через register_rest_field() или внутри метода prepare_item_for_response() контроллер проверяет, запросил ли клиент данное поле в параметре _fields. Если клиент передал ?_fields=id,title, ваш get_callback для поля reading_time даже не будет вызван, что экономит ресурсы CPU и запросы к БД.
Ниже представлен production-ready модуль на PHP 8.4+, который закрывает уязвимость перечисления пользователей (/wp/v2/users), регистрирует строго типизированные вычислимые поля с JSON Schema и управляет заголовками Edge CDN кэширования (Surrogate-Control / stale-while-revalidate).
<?php
/**
* Plugin Name: Fullstack Agentic Lab — Enterprise REST API Hardening & Edge Cache
* Description: Оптимизация Payload, защита эндпоинтов пользователей и HTTP Edge-кэширование для WP REST API.
* Version: 2.1.0
* Author: Artur Yusupov (Fullstack Agentic Lab)
*/
declare(strict_types=1);
namespace AgenticLab\Core\RestApi;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
if (!defined('ABSPATH')) {
exit;
}
final readonly class EnterpriseRestOptimizer
{
public static function init(): void
{
add_action('rest_api_init', self::registerComputedFields(...), 10);
add_filter('rest_endpoints', self::lockdownUserEnumerationEndpoints(...), 10, 1);
add_filter('rest_post_dispatch', self::injectEdgeCacheHeaders(...), 10, 3);
}
/**
* 1. Регистрация типизированного поля с JSON Schema.
* Вызывается ядром ТОЛЬКО если поле включено в параметр _fields!
*/
public static function registerComputedFields(): void
{
register_rest_field(['post', 'case_study'], 'reading_metrics', [
'get_callback' => static function (array $postArr): array {
$postId = (int) ($postArr['id'] ?? 0);
$rawContent = (string) get_post_field('post_content', $postId);
$wordCount = str_word_count(wp_strip_all_tags($rawContent));
$minutes = max(1, (int) ceil($wordCount / 220));
return [
'word_count' => $wordCount,
'estimated_minutes' => $minutes,
];
},
'update_callback' => null,
'schema' => [
'description' => 'Метрики объема и времени чтения материала',
'type' => 'object',
'context' => ['view', 'embed'],
'readonly' => true,
'properties' => [
'word_count' => ['type' => 'integer'],
'estimated_minutes' => ['type' => 'integer'],
],
],
]);
}
/**
* 2. Предотвращение утечки логинов администраторов (User Enumeration Attack)
* при сохранении работоспособности панели автора в Gutenberg для редакторов.
*
* @param array<string, mixed> $endpoints
* @return array<string, mixed>
*/
public static function lockdownUserEnumerationEndpoints(array $endpoints): array
{
if (current_user_can('list_users') || current_user_can('edit_posts')) {
return $endpoints;
}
unset(
$endpoints['/wp/v2/users'],
$endpoints['/wp/v2/users/(?P<id>[\\d]+)'],
$endpoints['/wp/v2/users/me']
);
return $endpoints;
}
/**
* 3. Инъекция заголовков Edge-кэширования (stale-while-revalidate) для публичных GET-запросов.
* Исключает кэширование персонализированных запросов с заголовком Authorization или X-WP-Nonce.
*/
public static function injectEdgeCacheHeaders(
WP_REST_Response $response,
WP_REST_Server $server,
WP_REST_Request $request
): WP_REST_Response {
if ($request->get_method() !== 'GET' || $response->get_status() !== 200) {
return $response;
}
$hasAuthHeader = $request->get_header('authorization') !== null
|| $request->get_header('x_wp_nonce') !== null;
if (is_user_logged_in() || $hasAuthHeader) {
$response->header('Cache-Control', 'private, no-store, no-cache, must-revalidate');
return $response;
}
// Разрешаем CDN (Cloudflare / Vercel) кэшировать публичный JSON на 5 минут с фоновым обновлением сутки
$response->header(
'Cache-Control',
'public, max-age=60, s-maxage=300, stale-while-revalidate=86400'
);
$response->header('Vary', 'Accept-Encoding, Origin');
return $response;
}
}
EnterpriseRestOptimizer::init();Что произойдет, если запросить ?_fields=id,title,slug&_embed без указания _links и _embedded в списке полей?
В чем суть ошибки: Разработчик вставляет сниппет из интернета, который на фильтре rest_authentication_errors проверяет if (!is_user_logged_in()) return new WP_Error('forbidden', ...) для всех маршрутов без исключения.
Почему разработчики так делают: Попытка «защитить сайт от хакеров», закрыв /wp-json/.
Что происходит в продакшене: Мгновенно ломаются публичные формы обратной связи (Contact Form 7, Gravity Forms), поиск по сайту, корзина и чекаут WooCommerce Store API (/wc/store/v1/cart), интерактивные блоки WordPress Interactivity API и вебхуки платежных систем.
Как правильно: Ограничивать доступ точечно — скрывать только чувствительные маршруты (например, /wp/v2/users для неавторизованных посетителей через фильтр rest_endpoints), оставляя публичные API работоспособными.
В чем суть ошибки: Фронтенд-разработчик запрашивает fetch('https://site.com/wp-json/wp/v2/posts?per_page=50') для рендеринга списка карточек блога, где нужны только заголовок, дата и ссылка.
Почему разработчики так делают: Незнание встроенной проекции полей ядра.
Что происходит в продакшене: WordPress для всех 50 записей выполняет тяжелую серверную компиляцию всех блоков Gutenberg в поле content.rendered, формирует десятки ссылок _links и отправляет клиенту JSON-файл весом 1.5–2.5 МБ. Показатели LCP и TBT на мобильных устройствах деградируют в разы.
Как правильно: Всегда указывать точную маску полей: /wp-json/wp/v2/posts?per_page=20&_fields=id,title,slug,date,excerpt,_links&_embed=wp:featuredmedia.
В чем суть ошибки: Настройка жесткого кэширования всех URL вида /wp-json/* в правилах Cloudflare Page Rules (Cache Everything) без проверки заголовков авторизации.
Почему разработчики так делают: Желание снизить нагрузку на сервер базы данных.
Что происходит в продакшене: Когда администратор открывает редактор Gutenberg, его браузер запрашивает /wp-json/wp/v2/posts/123?context=edit. Если CDN закэширует этот ответ и отдаст его следующему анонимному гостю, произойдет критическая утечка приватных данных, черновиков и личной информации (Cache Poisoning / Data Leak).
Как правильно: Отдавать публичный s-maxage ТОЛЬКО когда !is_user_logged_in() и отсутствуют заголовки Authorization / X-WP-Nonce, а для авторизованных запросов принудительно выставлять Cache-Control: private, no-store.
В чем суть ошибки: Получив массив из 12 постов, где указан 'featured_media': 405, фронтенд в цикле posts.map() запускает 12 дополнительных запросов GET /wp-json/wp/v2/media/405.
Почему разработчики так делают: Буквальное следование ссылкам ID без изучения спецификации HAL в WordPress.
Что происходит в продакшене: Вместо 1 запроса браузер или сервер Next.js генерирует лавину из 13–25 параллельных HTTP-соединений к PHP-FPM, вызывая троттлинг (HTTP 429) и задержку рендеринга.
Как правильно: Добавлять параметр ?_embed=wp:featuredmedia,author к основному запросу и считывать готовый URL изображения из post._embedded['wp:featuredmedia'][0].source_url.