Підсистема 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.
_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();У чому суть помилки: Розробник додає сніпет з інтернету, який на фільтрі 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.
Клієнт явно перераховує необхідні ключі 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.