Todo empieza con una alerta de Sentry un martes por la noche. Una consulta falló, el mensaje de la excepción está ahí mismo y dentro, impreso completo, aparece el email de un cliente y medio número de teléfono. Nadie registró eso a propósito. QueryException simplemente siempre ha interpolado los valores enlazados dentro del mensaje, y ese mensaje se copia a tus archivos de log, a la tabla failed_jobs, al APM al que envíes tus excepciones. Laravel 13.27 por fin le pone un interruptor a esto: mask_bindings_in_exception_messages, configurable por conexión, mantiene los marcadores ? en el texto del mensaje mientras getBindings() sigue devolviendo los valores reales cuando los pides de forma deliberada.
<?php
// config/database.php (excerpt)
return [
'connections' => [
'mysql' => [
'driver' => 'mysql',
'mask_bindings_in_exception_messages' => env('DB_MASK_BINDINGS', false),
],
],
];Ese flag es mi favorito de un tramo inusualmente denso: once versiones menores entre 13.17 y 13.27, publicadas en menos de tres meses. El plato fuerte es, obviamente, el nuevo componente de imagen oficial, y llegaré a él. Pero esta es mi postura: las funciones que justifican tu tarde de actualización son las defensivas, las que solo muestran su valor bajo tráfico concurrente o en mitad de un incidente, y la mayoría llegan desactivadas o se quedan en una línea del changelog que nadie retuitea. Si repasas estas versiones buscando cosas brillantes, pasarás de largo justo las novedades que te habrían ahorrado un postmortem.
El flag de enmascaramiento tiene compañía. La 13.23 hizo, sin hacer ruido, que la comparación del secreto para saltarse el modo mantenimiento use hash_equals() de forma consistente, cuando antes era resistente a ataques de temporización en algunas rutas del código y un simple === en otras. Cosas pequeñas, claro. Pero la higiene de logs y las comparaciones seguras frente a temporización son esa categoría de trabajo que nunca sale en una keynote y que decide si tu próxima auditoría de seguridad es aburrida. Yo pondría DB_MASK_BINDINGS a true en cada proyecto nuevo y, en los viejos, iría a hacer grep en la tabla failed_jobs buscando arrobas. Puede que no te guste lo que aparezca.
Luego está la familia de las condiciones de carrera. La 13.27 añade refreshForUpdate() a los modelos de Eloquent. Imagina un cupón con tres canjes restantes justo después de que salga el email de marketing: dos peticiones llegan al endpoint en los mismos cien milisegundos, ambas leen el contador, ambas ven hueco, ambas escriben, y has regalado más descuentos de los que el cupón permitía. El bloqueo pesimista arregla esto, y Eloquent lo soporta desde hace años, pero solo en el momento de la consulta. Cuando el modelo ya estaba en tus manos, el apaño era torpe: lanzar una segunda consulta con bloqueo dentro de la transacción y seguir con esa copia fresca mientras la variable original se quedaba ahí como un señuelo obsoleto esperando hacerte tropezar. refreshForUpdate() condensa ese baile en una sola llamada que relee el modelo que ya tienes con lockForUpdate() aplicado, dentro de la transacción donde un bloqueo significa algo de verdad. Carreras como esta prácticamente nunca se reproducen en un portátil con un solo usuario, y por eso una primitiva de una línea importa más de lo que parece. Ahí cerca, whereBinary() da a los usuarios de MySQL y MariaDB comparaciones sensibles a mayúsculas sin bajar a SQL crudo.
La historia del despliegue también mejoró. queue:pause --all y queue:resume --all detienen todas las colas con una sola llamada a artisan, en lugar de obligarte a perseguir nombres de colas que se desviaron desde la última vez que alguien los apuntó. schedule:work ahora gestiona SIGINT, SIGTERM y SIGQUIT con limpieza, algo que importa en cuanto Kubernetes reprograma tu pod con una tarea a medias. El evento WorkerStopping ahora lleva jobsProcessed y lastJobProcessedAt, así que el rendimiento por worker queda visible sin que tengas que atornillar contadores al bucle de jobs. Dos piezas de infraestructura encajan en el mismo patrón defensivo: la 13.17 le enseña a la configuración de pgsql la diferencia entre un endpoint con pooling (PgBouncer, RDS Proxy, Neon) y uno directo, y manda por su cuenta las migraciones y schema:dump por la conexión directa. Y el driver de sistema de archivos read-through de la 13.26 sirve desde un disco primario, recurre al disco antiguo cuando no encuentra el archivo y va copiando los ficheros a medida que se piden, lo que convierte una migración de proveedor de almacenamiento de un maratón de rsync de fin de semana en un proceso en segundo plano en el que casi dejas de pensar.
Ahora, la concesión honesta: el lado brillante de estas versiones es bueno. Illuminate\Image, introducido en la 13.20, envuelve Intervention Image v4 (sigues haciendo composer require intervention/image por tu cuenta, con GD o Imagick por debajo) y maduró rápido. Las subidas HEIC y HEIF desde iPhones se aceptan desde la 13.24 en lugar de rebotar, dominantColor() te da un fondo de placeholder para fotos con carga diferida, y como Image implementa Responsable puedes devolver una imagen transformada directamente desde una ruta. Las instancias son inmutables, así que una misma foto de origen puede dar un avatar pequeño aquí y un hero desenfocado allá sin que las transformaciones se contaminen entre sí. Esta es la función que los equipos adoptarán esta misma semana, y la adopción mantiene vivo un framework. No voy a fingir lo contrario.
También concedo el contraargumento a mi propia elección. Quien está de guardia a las 3 de la madrugada quiere los valores enlazados impresos en la excepción, porque el valor culpable suele ser el camino más corto hasta el bug, y el enmascaramiento pone un paso entre esa persona y la respuesta. Es verdad, y probablemente por eso el flag viene desactivado por defecto. Aun así, yo me quedo con activarlo: en el código que controlas, getBindings() te da los valores de todos modos, mientras que el texto del mensaje viaja hacia proveedores externos, políticas de retención y archivos de backup que no controlas en absoluto. La fricción al depurar se recupera. El teléfono de un cliente en el almacenamiento de logs de un tercero, no.
Hay mucho en este tramo sobre lo que no discutí: Validator::fakeDnsLookups() para que los tests con email:dns sobrevivan a redes inestables, el atributo #[RouteKey('slug')], una regla de validación base64, #[DebounceFor] en listeners de eventos en cola, soporte de HTTP QUERY vía Http::query(), incluso un driver de log mensual. Repasa el changelog tú mismo. Mi pregunta para ti es más concreta: cuando una consulta revienta en producción, ¿quieres los valores enlazados en el propio mensaje o detrás de una llamada deliberada a getBindings()? Yo ya elegí, y sospecho que la gente de guardia tiene historias capaces de poner a prueba mi elección. Cuéntame dónde te ha mordido este intercambio.




Comentarios
Aún no hay comentarios — escribe el primero.
Inicia la conversación
Sin cuenta ni contraseña — introduce tu correo y te enviamos un enlace de acceso de un solo uso. ¿Primera vez? Todo se configura automáticamente.
Tu valoración se aplicará automáticamente al iniciar sesión.
Revisa tu bandeja de entrada
Hemos enviado un enlace de acceso a …. Ábrelo en este dispositivo — esta pestaña te conectará automáticamente.
¿No llega nada? Mira en la carpeta de spam — y marca el correo como «No es spam» para que la próxima vez llegue directo.