Como crear un endpoint proxy para APIs externas y evitar CORS en WordPress

Contents

Introducción

En este tutorial se explica de forma detallada cómo crear en WordPress un endpoint proxy que actúe como intermediario entre el navegador y APIs externas para evitar problemas de CORS. La técnica consiste en que el navegador solicite a tu propio dominio (sin CORS) y tu servidor (WordPress) solicite a la API externa, devolviendo la respuesta al navegador. Se incluyen ejemplos completos en PHP para registrar la ruta REST, validar y sanitizar peticiones, manejar headers, caché, errores, y un ejemplo de uso desde JavaScript.

¿Por qué usar un proxy en WordPress?

  • Evita restricciones CORS: El navegador hace la petición a tu mismo dominio (sin política CORS), y tu servidor hace la petición a la API externa.
  • Oculta claves y credenciales: Los tokens o claves se mantienen en el servidor y no viajan al cliente.
  • Control y seguridad: Puedes imponer lista blanca de dominios, limitar tasas, filtrar respuestas y aplicar caché.
  • Compatibilidad: Funciona con APIs que no permiten CORS o que requieren cabeceras complejas.

Consideraciones de seguridad antes de empezar

  • Nunca implementar un proxy abierto que permita cualquier URL: obliga siempre a una allowlist (lista blanca) o reglas precisas.
  • Sanitiza y valida la URL y parámetros.
  • Limita tamaño de respuesta y tiempo de conexión para evitar abuso y consumo de memoria.
  • Usa caching y rate-limiting para controlar tráfico hacia APIs externas y mitigar costes o bloqueos.
  • Si necesitas exponer credenciales para la API externa, guárdalas en constantes, opciones seguras o variables de entorno y no en el cliente.

Implementación paso a paso (plugin o functions.php)

Ejemplo completo para registrar un endpoint REST en WordPress. Este código puede colocarse en un plugin o en functions.php de un child theme (recomendado: plugin para mayor control).

1) Registro de la ruta REST y permisos

 WP_REST_Server::READABLE . , . WP_REST_Server::CREATABLE . , . WP_REST_Server::EDITABLE . , . WP_REST_Server::DELETABLE,
        callback => wp_proxy_request_handler,
        permission_callback => wp_proxy_permission_check,
    ))
})

/
  Permission callback: aquí se controla quién puede usar el proxy.
  - Devuelve true para permitir acceso público (riesgo mayor).
  - Mejor: implementar controles (token, usuario logueado, referer, etc.).
 /
function wp_proxy_permission_check(WP_REST_Request request) {
    // Ejemplo seguro: permitimos peticiones públicas pero solo a hosts de la allowlist.
    // La validación de la URL real se hace en el handler.
    return true
}
?>

2) Handler principal: validación, petición a la API externa, respuesta

Este handler realiza:

  1. Recoge la URL objetivo (param url).
  2. Valida y checa la lista blanca de hosts.
  3. Prepara argumentos para wp_remote_request: método, headers seguros, body.
  4. Aplica caché para GET usando transients.
  5. Devuelve la respuesta con el mismo Content-Type y status code de la API externa y añade cabeceras CORS si es necesario.
get_param(url)
    if (empty(url)) {
        return new WP_REST_Response(array(error => missing_url), 400)
    }

    // Sanitizar
    url = esc_url_raw(url)
    if (empty(url)) {
        return new WP_REST_Response(array(error => invalid_url), 400)
    }

    // Lista blanca de hosts (ajusta a tus necesidades)
    allowed_hosts = array(
        api.example.com,
        another.service.net,
        raw.githubusercontent.com,
    )
    parsed = wp_parse_url(url)
    if (empty(parsed[host])  ! in_array(parsed[host], allowed_hosts, true)) {
        return new WP_REST_Response(array(error => host_not_allowed), 403)
    }

    // Determinar método HTTP y preparar args
    method = strtoupper(request->get_method()) // GET, POST, PUT, DELETE, etc.
    args = array(
        method      => method,
        timeout     => 20,
        redirection => 5,
        httpversion => 1.1,
    )

    // Copiar headers seguros desde la petición original
    incoming_headers = request->get_headers()
    forward_headers = array()
    allowed_forward = array(authorization, content-type, accept) // ajustar lista
    foreach (incoming_headers as key => value) {
        k = strtolower(key)
        if (in_array(k, allowed_forward, true)) {
            // WP REST headers vienen en arrays
            forward_headers[k] = is_array(value) ? implode(, , value) : value
        }
    }
    if (! empty(forward_headers)) {
        args[headers] = forward_headers
    }

    // Pasar body para métodos no GET
    if (method !== GET) {
        body = request->get_body()
        if (! empty(body)) {
            args[body] = body
        } else {
            // Para formularios
            params = request->get_params()
            if (! empty(params)) {
                args[body] = params
            }
        }
    }

    // CACHÉ para GET (opcional)
    cache_key = proxy_ . md5(url . serialize(args))
    if (method === GET) {
        cached = get_transient(cache_key)
        if (cached !== false) {
            // Devolver respuesta cacheada (asumimos array con body,headers,status)
            resp = new WP_REST_Response(cached[body], cached[status])
            if (! empty(cached[headers])  is_array(cached[headers])) {
                foreach (cached[headers] as hk => hv) {
                    resp->header(hk, hv)
                }
            }
            // Permitir CORS desde todas las fuentes o ajustar dominio
            resp->header(Access-Control-Allow-Origin, )
            return resp
        }
    }

    // Petición hacia la API externa
    remote = wp_remote_request(url, args)
    if (is_wp_error(remote)) {
        error = remote->get_error_message()
        return new WP_REST_Response(array(error => request_failed, message => error), 502)
    }

    status = wp_remote_retrieve_response_code(remote)
    body = wp_remote_retrieve_body(remote)
    response_headers = wp_remote_retrieve_headers(remote) // array of headers

    // Construir respuesta REST con mismo body y Content-Type
    resp = new WP_REST_Response(body, status)

    // Establecer Content-Type si viene de la API externa
    if (! empty(response_headers[content-type])) {
        resp->header(Content-Type, response_headers[content-type])
    } else {
        // Forzar tipo si no viene
        resp->header(Content-Type, application/octet-stream)
    }

    // Reenviar otras cabeceras útiles (solo algunas seguras)
    pass_through = array(cache-control, expires, etag)
    foreach (pass_through as hk) {
        if (! empty(response_headers[hk])) {
            resp->header(hk, response_headers[hk])
        }
    }

    // Permitir CORS (ajusta a tu dominio en producción)
    resp->header(Access-Control-Allow-Origin, )
    resp->header(Access-Control-Allow-Methods, GET, POST, OPTIONS)
    resp->set_status(status)

    // Guardar en caché si es GET y status 200
    if (method === GET  status === 200) {
        cache_value = array(
            body    => body,
            headers => array_intersect_key(response_headers, array_flip(pass_through)),
            status  => status,
        )
        // TTL configurable, ejemplo 5 minutos
        set_transient(cache_key, cache_value, MINUTE_IN_SECONDS  5)
    }

    return resp
}
?>

3) Limitar tasa por IP (rate limit simple)

Ejemplo sencillo que bloquea si hay más de X peticiones en Y segundos por IP usando transients:

 1, t => time())
        set_transient(key, data, 60) // ventana 60s
        return true
    }
    data[count]  
    set_transient(key, data, 60)
    // límite: 30 peticiones por minuto
    if (data[count] > 30) {
        return false
    }
    return true
}

// Uso dentro del handler:
if (! wp_proxy_rate_limit_check()) {
    return new WP_REST_Response(array(error => rate_limited), 429)
}
?>

Ejemplo de uso desde JavaScript (cliente)

Ejemplo para consumir el proxy desde el navegador usando fetch. Nota: el navegador se conecta a tu dominio /wp-json/… y no hay problemas de CORS si el frontend y backend comparten origen.

// GET simple (esperando JSON)
const target = https://api.example.com/data?param=value
fetch(/wp-json/proxy/v1/request?url=   encodeURIComponent(target), {
  method: GET,
  credentials: same-origin // opcional según si necesitas cookies
})
.then(response => {
  const ct = response.headers.get(content-type)  
  if (ct.includes(application/json)) {
    return response.json()
  }
  return response.text()
})
.then(data => {
  console.log(Proxy result:, data)
})
.catch(err => console.error(Proxy error:, err))

Enviar POST con JSON y recibir respuesta

const target = https://api.example.com/resource
fetch(/wp-json/proxy/v1/request, {
  method: POST,
  headers: { Content-Type: application/json },
  body: JSON.stringify({ url: target, payload: { key: value } })
})
.then(r => r.json())
.then(json => console.log(json))

Detalles, buenas prácticas y casos especiales

Sobre evitar CORS

La mecánica del proxy evita necesitar que la API externa permita CORS: el navegador nunca habla directamente con la API externa, lo hace con tu dominio. Aun así, el proxy puede querer añadir cabeceras Access-Control-Allow-Origin para que clientes en otros orígenes puedan consumir tu endpoint si publicas una web en otro dominio (pero normalmente el frontend y el WP están en el mismo dominio).

Encabezados y seguridad

  • No reenvíes indiscriminadamente cookies ni headers que puedan comprometer tu sistema.
  • Reenvía solo headers necesarios (Authorization opcional si controlas su uso).
  • Controla y filtra URLs: permitiendo solo hosts concretos o patrones.

Archivos binarios y streaming

Si necesitas descargar imágenes/archivos grandes, la implementación con WP_REST_Response y wp_remote_request puede consumir memoria. Para contenido grande puedes:

  • Usar wp_remote_request y devolver directamente el body si cabe en memoria.
  • Para streaming real (sin cargar todo en memoria), usar un endpoint PHP que abra un stream desde la URL remota y haga echo de bloques, ajustando Content-Type y Content-Length y terminando con exit esto suele hacerse fuera del sistema REST o con cuidado en un endpoint propio.

Autenticación hacia APIs externas

Si la API externa requiere token, almacénalo en opciones seguras o constantes y aplícalo desde el servidor al construir args[headers][Authorization] = Bearer … así el token nunca llega al cliente.

Manejo de errores y códigos HTTP

Devuelve siempre el código HTTP real recibido de la API externa (o uno representativo) para que el cliente pueda reaccionar correctamente. Encapsula errores de wp_remote_request en 5xx o 502 para indicar fallo del proxy.

Logs y monitorización

Registra errores importantes en los logs para auditar problemas: utiliza error_log() o plugins de logging. Monitorea latencias y tasas de error para detectar problemas de la API externa o abusos del proxy.

Tabla resumen: qué reenviar del servicio remoto y qué bloquear

Reenviar Bloquear / No reenviar
Content-Type, Cache-Control, Expires Set-Cookie, Server (información interna), X-Powered-By
ETag (opcional, para caching) Cabeceras que contengan tokens sensibles

Resumen final

Crear un endpoint proxy en WordPress es una solución práctica y potente para esquivar problemas CORS, proteger credenciales y controlar el flujo hacia APIs externas. La clave está en validar/limitar las URLs permitidas, sanitizar datos, aplicar caché, y proteger el endpoint contra abuso (rate-limiting, logging y listas blancas). El ejemplo proporcionado ofrece una base sólida que puedes adaptar: mejorar el permiso de acceso, implementar un proxy de streaming para archivos grandes, añadir autenticación por token o integrar Redis para caché a mayor escala.



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *