El subsistema WP REST API (núcleo wp-includes/rest-api/) convierte a WordPress en un servidor de datos HTTP fuertemente tipado, orquestado por el despachador WP_REST_Server. A diferencia del renderizado de plantillas clásico (template-loader.php), al recibir peticiones con el prefijo /wp-json/, el analizador WP::parse_request() detecta la variable de entorno rest_route y redirige el flujo de ejecución hacia rest_api_loaded(), omitiendo por completo la carga de temas HTML.
El pipeline interno de WP_REST_Server::serve_request() ejecuta un ciclo de procesamiento de 5 fases:
WP_REST_Server::check_authentication): Dispara el filtro rest_authentication_errors. Admite de forma nativa Cookie + Nonce (X-WP-Nonce para consultas del editor Gutenberg en navegador), Application Passwords (Basic Auth RFC 7617 sobre HTTPS) y tokens Bearer externos (JWT / OAuth 2.0).WP_REST_Server::match_request_to_handler): Las expresiones regulares de rutas registradas en el hook rest_api_init se contrastan con el URI y el método HTTP (GET, POST, PUT, PATCH, DELETE), instanciando un objeto WP_REST_Request.WP_REST_Request::has_valid_params y sanitize_params): Antes de ejecutar la lógica de negocio, los parámetros entrantes se validan contra declaraciones de JSON Schema (type, format, enum, minimum, pattern) y callbacks de validate_callback / sanitize_callback.permission_callback): Predicado obligatorio de acceso. Si retorna false o un WP_Error, la ejecución se interrumpe de inmediato con un código 401 Unauthorized o 403 Forbidden.rest_post_dispatch): El controlador devuelve un WP_REST_Response. El filtro rest_post_dispatch aplica la proyección de campos (_fields=id,title,slug), resuelve recursos enlazados (_embed bajo la especificación HAL — Hypertext Application Language) y emite la respuesta JSON final.La WP REST API debe concebirse como una pasarela de serialización estrictamente tipada que proyecta el grafo de entidades de WordPress al formato HAL+JSON mediante filtrado selectivo de campos (_fields) y resolución de relaciones por lotes (_embed).
[Petición HTTP: GET /wp-json/wp/v2/posts?_fields=id,title,_links&_embed=wp:featuredmedia]
│
▼
[1. WP::parse_request() -> rest_api_loaded()]
│
▼
[2. Filtro rest_authentication_errors]
├── Validación X-WP-Nonce / Application Password / Bearer JWT
└── En caso de error ──► 401/403 WP_Error (Salida Rápida)
│
▼
[3. WP_REST_Server::dispatch(WP_REST_Request)]
├── Match Regex: /wp/v2/posts
├── Validación JSON Schema: validate_callback -> sanitize_callback
└── Autorización: permission_callback() === true
│
▼
[4. WP_REST_Posts_Controller::get_items()]
├── Optimización: ¡omite renderizar content si no está en _fields!
└── Retorno de WP_REST_Response
│
▼
[5. HAL Embedding y Filtrado de Campos (rest_post_dispatch)]
├── Resolución de _links['wp:featuredmedia'] -> _embedded
└── Recorte de claves JSON según la máscara _fieldsEjecución del pipeline rest_authentication_errors. Identificación del usuario mediante Nonce, App Password o JWT.
Emparejamiento de expresiones regulares y validación de argumentos contra la JSON Schema declarativa.
Ejecución de permission_callback en la ruta. Garantiza el aislamiento del endpoint antes de consultar la BD.
Invocación de métodos del controlador. Evaluación de _fields vía rest_is_field_included() para evitar cálculos pesados.
Incrustación de entidades vinculadas (_embed), adición de cabeceras de paginación (X-WP-Total) y emisión de JSON.
_fields y rest_is_field_included(): Al solicitar /wp-json/wp/v2/posts?_fields=id,title,slug, el tamaño de respuesta para 20 entradas desciende de 180 KB a 3.2 KB, mientras el servidor omite cientos de filtros the_content y renderizados de bloques Gutenberg._embed: El parámetro ?_embed=author,wp:featuredmedia,wp:term permite al frontend obtener las entradas junto con avatares de autores, imágenes destacadas y términos en 1 única ronda de red en vez de 4 consultas secuenciales.rest_preload_api_request): Precargar el estado inicial de endpoints REST en la caché global de wp.apiFetch.createPreloadingMiddleware permite que las aplicaciones React se hidraten de inmediato sin spinners de carga./wp/v2/users (prevención de ataques de enumeración de usuarios — User Enumeration) y adición de cabeceras estrictas Cache-Control: public, s-maxage=3600, stale-while-revalidate=86400 para CDN perimetrales.Comparemos las características arquitectónicas de la WP REST API nativa frente a WPGraphQL al diseñar arquitecturas desacopladas (Headless):
rest_is_field_included()#En versiones modernas del núcleo, al registrar un campo mediante register_rest_field() o dentro de prepare_item_for_response(), el controlador comprueba si el cliente solicitó explícitamente dicho campo en _fields. Si el cliente indicó ?_fields=id,title, tu get_callback para un campo pesado como reading_time ni siquiera será ejecutado, ahorrando ciclos de CPU y consultas a la base de datos.
A continuación se presenta un módulo de grado productivo en PHP 8.4+ que suprime la vulnerabilidad de enumeración de usuarios (/wp/v2/users), registra campos computados fuertemente tipados con JSON Schema y gestiona cabeceras de caché perimetral en CDN (Surrogate-Control / stale-while-revalidate).
<?php
/**
* Plugin Name: Fullstack Agentic Lab — Enterprise REST API Hardening & Edge Cache
* Description: Optimización de Payload, protección de endpoints de usuario y caché HTTP Edge para 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. Registro de campo tipado con JSON Schema.
* ¡El núcleo solo lo invoca si el campo está presente en el parámetro _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' => 'Métricas de volumen y tiempo estimado de lectura',
'type' => 'object',
'context' => ['view', 'embed'],
'readonly' => true,
'properties' => [
'word_count' => ['type' => 'integer'],
'estimated_minutes' => ['type' => 'integer'],
],
],
]);
}
/**
* 2. Prevención de fugas de nombres de usuario (Ataque de User Enumeration)
* manteniendo el selector de autor funcional en Gutenberg para editores.
*
* @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. Inyección de cabeceras de caché perimetral (stale-while-revalidate) para peticiones GET públicas.
* Excluye peticiones personalizadas que porten cabeceras Authorization o 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;
}
// Permite a CDN (Cloudflare / Vercel) almacenar JSON público por 5 minutos con revalidación de 24h
$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();En qué consiste el error: Añadir un fragmento en el filtro rest_authentication_errors que retorna if (!is_user_logged_in()) return new WP_Error('forbidden', ...) para todas las rutas indiscriminadamente.
Por qué lo cometen los desarrolladores: Intento equivocado de "proteger el sitio de bots y raspadores" bloqueando /wp-json/.
Qué ocurre en producción: Colapsan de inmediato formularios públicos de contacto (Contact Form 7, Gravity Forms), el buscador del sitio, el carrito y checkout de WooCommerce Store API (/wc/store/v1/cart), componentes de WordPress Interactivity API y webhooks de pasarelas de pago.
La solución correcta: Restringir el acceso de manera quirúrgica — ocultar únicamente rutas sensibles (como /wp/v2/users para no autenticados en el filtro rest_endpoints), preservando los endpoints operativos públicos.
En qué consiste el error: Invocar fetch('https://site.com/wp-json/wp/v2/posts?per_page=50') para construir tarjetas de vista previa donde solo se precisa título, fecha y slug.
Por qué lo cometen los desarrolladores: Desconocimiento de las capacidades nativas de proyección de campos del núcleo.
Qué ocurre en producción: Para los 50 artículos, WordPress ejecuta la costosa compilación en servidor de todos los bloques Gutenberg en content.rendered, genera decenas de enlaces _links y transfiere un archivo JSON masivo de 1.5–2.5 MB. Las métricas LCP y TBT en dispositivos móviles se degradan notablemente.
La solución correcta: Indicar siempre una lista explícita de campos requeridos: /wp-json/wp/v2/posts?per_page=20&_fields=id,title,slug,date,excerpt,_links&_embed=wp:featuredmedia.
En qué consiste el error: Configurar reglas agresivas de "Cache Everything" para /wp-json/* en Cloudflare Page Rules sin aislar cabeceras de sesión o autorización.
Por qué lo cometen los desarrolladores: Intentar reducir la carga sobre el servidor de base de datos.
Qué ocurre en producción: Cuando un editor abre Gutenberg, el navegador solicita /wp-json/wp/v2/posts/123?context=edit. Si la CDN almacena esta respuesta y la entrega a un usuario anónimo subsiguiente, se produce una fuga crítica de borradores y metadatos privados (Cache Poisoning / Fuga de Datos).
La solución correcta: Emitir cabeceras s-maxage públicas ÚNICAMENTE cuando !is_user_logged_in() y no existan cabeceras Authorization ni X-WP-Nonce, asignando obligatoriamente Cache-Control: private, no-store a peticiones autenticadas.
En qué consiste el error: Tras recibir 12 entradas con 'featured_media': 405, iterar con posts.map() disparando 12 peticiones individuales GET /wp-json/wp/v2/media/405.
Por qué lo cometen los desarrolladores: Seguimiento literal de identificadores sin aprovechar la especificación HAL embebida en WordPress.
Qué ocurre en producción: En lugar de 1 petición, el navegador o el servidor Next.js genera una avalancha de 13 a 25 conexiones concurrentes hacia PHP-FPM, provocando estrangulamiento por tasa de peticiones (HTTP 429) y demorando el renderizado.
La solución correcta: Agregar ?_embed=wp:featuredmedia,author a la petición principal y consumir la URL directamente desde post._embedded['wp:featuredmedia'][0].source_url.
El cliente especifica las claves JSON necesarias (ej: _fields=id,title,slug,reading_metrics,_links,_embedded).
Elimina hasta el 80% del tráfico superfluo y desactiva el cómputo de campos no utilizados en el servidor.
El núcleo resuelve de forma automatizada los objetos vinculados de medios, autores y taxonomías en _embedded.
Erradica por completo el patrón de peticiones en cascada N+1 en el cliente.
Aislamiento de rutas vulnerables (/wp/v2/users) para invitados y verificación de tokens de acceso.
Protege la plataforma frente a ataques de User Enumeration y scraping malicioso.
Extracción de metadatos de paginación desde las cabeceras HTTP X-WP-Total y X-WP-TotalPages.
Permite estructurar paginaciones fiables sin requerir consultas secundarias de conteo.
Almacenamiento de respuestas GET públicas en nodos CDN con entrega global en menos de 20 ms.
Las peticiones autorizadas omiten la caché limpiamente mediante Cache-Control: private.