Diagnóstico y solución: Correo Argentino, WooCommerce y el error fatal de HPOS



Si tenés una tienda WooCommerce en Argentina con HPOS (High-Performance Order Storage) activo y usás el plugin oficial de Correo Argentino para gestionar tus envíos, es muy probable que te haya pasado esto: entrás a WooCommerce → Pedidos → Añadir pedido, hacés clic en «Crear», y la pantalla te devuelve un error crítico genérico, sin ninguna pista de qué pasó.

Encontramos la causa raíz —en realidad, dos bugs distintos escondidos en el mismo plugin— y las corregimos de punta a punta. Compartimos el diagnóstico completo, el fix, y dejamos el plugin corregido disponible para descarga pública, para que cualquier otro negocio que use Correo Argentino con WooCommerce no tenga que pasar por el mismo dolor de cabeza.

En una frase: el plugin todavía tiene código escrito para el modelo de almacenamiento de pedidos viejo de WooCommerce (anterior a HPOS). Con HPOS activo y PHP 8, esos supuestos viejos generan un error fatal real al crear un pedido manualmente desde el panel — algo que nunca pasa en una compra normal de un cliente.



El síntoma: crear un pedido manual rompe la página

El primer indicio fue simple: cada vez que intentábamos crear un pedido manualmente desde el panel de administración —algo habitual cuando un cliente hace un pedido por WhatsApp o teléfono y hay que registrarlo a mano— la pantalla devolvía:

   
WordPress — Error crítico
Se ha producido un error crítico en este sitio web.
Por favor, comprobá la bandeja de entrada del correo electrónico
del administrador del sitio para recibir instrucciones.

Lo llamativo: las ventas reales de la tienda, hechas por un cliente desde el frontend, funcionaban perfecto. El problema estaba acotado, con precisión quirúrgica, a un solo botón de un solo lugar del panel.



El diagnóstico: por qué no aparecía nada en ningún log

Lo más difícil de este caso no fue encontrar el bug — fue encontrar dónde mirar. El error no quedaba registrado ni en el log de WordPress, ni en el log de PHP-FPM, ni en el de memoria, ni en ningún sistema de auditoría del servidor. Descartamos, en orden, cada sospechoso habitual:

  • Límite de memoria de PHP — descartado, subimos el límite y el error persistió igual.
  • Wordfence interceptando algo — descartado, desactivamos el plugin por completo y el error siguió igual.
  • AppArmor bloqueando el proceso — descartado, sin coincidencias en los logs del kernel.
  • El OOM killer matando el proceso por falta de memoria — descartado, sin registros en dmesg.
  • Un timeout de ejecución — descartado, el error ocurría casi instantáneamente.

La pista real apareció en el log de error de Apache, no en ningún log de WordPress: ahí, mezclado entre el resto del tráfico, encontramos un notice de WooCommerce señalando directamente al plugin de Correo Argentino.



Bug 1: el metabox que usaba una propiedad que ya no existe

El primer hallazgo fue en Orders/correoargentino-metabox.php, el código que dibuja el cuadro de «Correo Argentino» en la pantalla de edición de pedidos:

   
Orders/correoargentino-metabox.php — ANTES
public static function content($post, $metabox)
{
    $order = wc_get_order($post->ID);                              // (A)
    $currentServiceType = Utils::getCurrentServiceType();
    $chosenServiceType = $order->get_meta(CA_CHOSEN_SERVICE_TYPE);  // (B) usa $order
    $orderServiceType = Utils::getOrderServiceType($post->ID);

    if (empty($order)) {                                            // (C) valida DESPUÉS de (B)
        return false;
    }
    ...

Dos problemas en estas pocas líneas:

1. $post->ID (línea A) asume un objeto WP_Post clásico. En la pantalla HPOS de WooCommerce (woocommerce_page_wc-orders), el callback del metabox recibe directamente un objeto WC_Order, no un WP_Post. Acceder a ->ID sobre un WC_Order funciona solo gracias a una capa de compatibilidad legacy de WooCommerce, que además emite un aviso en el log cada vez que se usa:

   
error.log
PHP Notice: La función ID fue llamada de forma incorrecta.
Order properties should not be accessed directly.
Backtrace: ... CorreoArgentinoMetabox::content,
WC_Abstract_Legacy_Order->__get, wc_doing_it_wrong

2. La validación empty($order) (línea C) está después de haber usado $order (línea B). Si wc_get_order() devuelve false —algo que puede pasar en el instante exacto en que un pedido todavía no está completamente resuelto bajo HPOS— la línea B ejecuta efectivamente false->get_meta(...). En PHP 8 eso es un error fatal no capturado, no una advertencia silenciosa como hubiera sido en PHP 7.

La corrección

   
Orders/correoargentino-metabox.php — DESPUÉS
public static function content($post_or_order, $metabox)
{
    // Acepta tanto WP_Post (legacy) como WC_Order (HPOS)
    if ($post_or_order instanceof WC_Order) {
        $order = $post_or_order;
    } else {
        $order = wc_get_order($post_or_order->ID);
    }

    // Validar ANTES de usar $order en cualquier llamada a método
    if (empty($order)) {
        return false;
    }

    $order_id = $order->get_id(); // en vez de $post->ID
    ...

El resto del método queda exactamente igual; solo se reemplazaron las apariciones de $post->ID por $order_id, y se invirtió el orden de la validación.



Bug 2: la causa real, escondida un nivel más abajo

Corregir el primer bug no resolvió el problema — el error 500 seguía ahí, igual de silencioso. Hizo falta instrumentar el propio código del plugin con líneas de diagnóstico temporal (error_log()) en cada paso de la cadena de guardado de WooCommerce, hasta encontrar el punto exacto donde la ejecución moría. Esta fue la línea que lo confirmó, capturada en el log real de Apache durante una prueba:

   
error.log — el momento exacto de la falla
[proxy_fcgi:error] Got error 'PHP message:
CAB-DEBUG: handle_order_update() ENTRO. POST action=edit_order order_id=2028;
PHP message: CAB-DEBUG2: order_status=wc-pending payment_method=;
PHP message: CAB-DEBUG3: new_order_handler INICIO order_id=2028'

Después de esa última línea, nada. Ni un fatal error, ni un warning — el script moría en absoluto silencio, en el medio de la función new_order_handler(), en correoargentino-shipping.php:

   
correoargentino-shipping.php — ANTES
function new_order_handler($order_id)
{
    $order = new WC_Order($order_id);                       // (A)
    $current_branch = WC()->session->get(CA_CHOSEN_BRANCH);  // (B) ← muere aquí
    ...

Esta función está enganchada al hook woocommerce_new_order, que WooCommerce dispara cuando un pedido pasa de un estado borrador (auto-draft) a un estado real — esto pasa tanto en una compra real del frontend como al crear un pedido manualmente desde el admin.

El código asumía que WC()->session está siempre disponible e inicializada. Eso es cierto durante el checkout del frontend, donde WooCommerce siempre inicializa la sesión — pero no en el panel de administración, donde la sesión de WooCommerce normalmente nunca se inicializa. Llamar a WC()->session->get(...) cuando la sesión no existe genera un Fatal Error inmediato que corta la ejecución sin dejar ningún rastro útil en los logs habituales.

La corrección

   
correoargentino-shipping.php — DESPUÉS
function new_order_handler($order_id)
{
    $order = wc_get_order($order_id);

    if (!$order) {
        return;
    }

    // Si no hay sesión activa (contexto de admin al crear un pedido
    // manualmente), no hay datos de sucursal para guardar. Salir
    // sin error, sin afectar el guardado del pedido.
    if (!WC()->session || !WC()->session->has_session()) {
        return;
    }

    $current_branch = WC()->session->get(CA_CHOSEN_BRANCH);
    ...
    $order->save();
    Utils::unset_wc_chosen_branch_values();
}

Dos cambios puntuales: wc_get_order() en vez de new WC_Order() (la forma correcta y soportada de obtener un pedido en cualquier modelo de almacenamiento), y una verificación de que la sesión exista y esté activa antes de leer cualquier dato de ella. En el flujo normal de checkout —donde la sesión siempre existe— el comportamiento queda idéntico al original.



El resultado

Ambos fixes son acotados y de bajo riesgo — no tocan nada de la lógica de negocio del plugin, solo corrigen el orden de validación y la forma de acceder a datos que cambiaron de comportamiento bajo HPOS. El resultado: la creación manual de pedidos funciona con normalidad, y el comportamiento del plugin en el checkout real de la tienda —donde todo funcionaba bien desde antes— queda exactamente igual, sin ningún cambio de funcionalidad.

Lo confirmamos con cinco pruebas finales: un correo genérico de sistema, un correo de restablecimiento de contraseña, un pedido creado manualmente desde el panel, un pedido real hecho desde el frontend, y la notificación de pedido nuevo generada automáticamente — los cinco funcionando correctamente, sin errores.



Plugin corregido, disponible para descarga

Dejamos el plugin con ambos fixes aplicados disponible públicamente, por si a alguien más le sirve mientras el desarrollador original evalúa incorporar la corrección a la versión oficial:

Descargar plugin corregido

Nota: esta es una corrección no oficial aplicada por CAB Group SRL. Se reportó el bug y la solución completa al desarrollador original del plugin para su evaluación.



La lección de fondo

Este caso es un buen ejemplo de por qué la migración a HPOS, aunque trae mejoras reales de performance para tiendas grandes, también puede dejar en evidencia código de plugins de terceros que nunca se actualizó para el nuevo modelo de datos. Y también es un ejemplo de por qué, cuando un error no deja ningún rastro en los logs habituales, vale la pena instrumentar el propio código con diagnóstico temporal en lugar de seguir adivinando — a veces la respuesta está exactamente donde menos se la espera, en un detalle de una sola línea.