Whitepaper

Fallas silenciosas en la costura desacoplada

En una sola versión corregimos tres defectos en el límite entre un CMS Drupal y su front end Next.js. Ninguno produjo un error. Los tres eran invisibles para CI, los tipos y las comprobaciones de salud. Por eso ese límite falla en silencio, y esto es lo que hicimos para que falle de forma visible.
July 15, 2026
Topics:Headless CMSSecurityAccess ControlCI/CDContent Modeling
Tags:DrupalNext.jsGraphQLTypeScript

Tres defectos de producción en el mismo límite, en la misma versión, ninguno de los cuales produjo un mensaje de error. Lo que realmente cuesta el desacoplamiento, por qué la canalización habitual no puede verlo, y los tres artefactos que hicieron comprobable el límite.

La costura de la que nadie es dueño

Desacoplar un CMS de su front end divide un sistema en dos, cada uno con su propio repositorio, pruebas, canalización de despliegue y guardia. La división vale la pena. Lo que crea, y lo que casi nadie presupuesta, es una costura: un contrato entre las dos mitades que existe en producción y en ningún otro sitio.

Dentro de cada mitad, las herramientas son buenas. Drupal tiene una opinión sobre si Drupal es correcto. TypeScript tiene una opinión sobre si el front end es internamente coherente. Ambos son honestos. Ninguno tiene opinión alguna sobre el otro.

Así que la costura tiene una propiedad que el resto del sistema no tiene: es el único lugar donde ambos lados pueden ser individualmente correctos y conjuntamente estar rotos. Y como ninguna prueba en ninguno de los dos repositorios la está mirando, las fallas no se anuncian. Esperan a que una persona se dé cuenta.

En una sola versión lanzamos correcciones para tres de ellas. Vale la pena recorrer las tres, porque parecen no tener relación y sí la tienen.

Falla uno: la vista previa que servía contenido publicado

Los editores hacían clic en Vista previa en Drupal y recibían la página que ya estaba en vivo. La URL de vista previa firmada se validaba. El modo borrador se activaba. La ruta se renderizaba. El banner que anunciaba "está viendo un borrador" aparecía sobre un contenido que no era un borrador.

La causa era un cargador. La ruta de vista previa leía la bandera de modo borrador y la usaba para decidir si un nodo no publicado debía devolver 404, pero nunca para decidir qué obtener. Llamaba a la misma consulta que llama el sitio público, que pide a Drupal un nodo por ruta y recibe la revisión predeterminada. Para un nodo publicado, la revisión predeterminada es la publicada. Un borrador de un nodo publicado es una revisión adelantada: más reciente, no predeterminada, deliberadamente invisible para el tráfico anónimo.

La consulta se comportaba exactamente como se especificó. El sistema de vista previa estaba completo salvo por la única capacidad para la que existía.

Observe lo que ve un linter aquí: una variable que se usa. El código sin usar se señala. El código usado para el propósito equivocado parece código que funciona.

Falla dos: el contrato que podía borrar todas las páginas a la vez

Renombre un campo en Drupal y el front end no se degrada: se detiene.

GraphQL valida una consulta como documento completo antes de ejecutar cualquier parte de ella. Un campo seleccionado que ya no existe no devuelve datos parciales con una advertencia; rechaza la consulta entera. Nuestra consulta de nodos es un único documento de diez mil caracteres detrás de todas las páginas de contenido del sitio. Una selección obsoleta en ella tumba todas las páginas que la ejecutan, simultáneamente, en producción.

Nos ha pasado dos veces. Ambos despliegues estaban en verde.

La falla especular es aún más silenciosa. Añada un campo en Drupal y el front end simplemente nunca lo ve. No hay error, ni síntoma, ni límite superior al tiempo que la deriva puede permanecer ahí. El caso aditivo no tiene ningún modo de falla, que es precisamente por lo que se acumula.

Falla tres: la regla de acceso que no estaba donde nadie miraba

La cuenta de servicio de nuestro front end tenía el rol administrator. Eso se lee como un hallazgo grave: un cliente de cara al público autenticándose desde una cuenta de administrador.

No lo era, y la razón importa. Con simple_oauth en granularidad por rol, los roles de la cuenta se eliminan de un token client_credentials. El alcance es el límite. Lo confirmamos con una cuenta y dos consumidores: mismo usuario, roles []; el alcance previewer devuelve el borrador no publicado, el alcance nextjs_site devuelve null. Identidad idéntica, respuesta distinta.

Así que el rol de administrador no había estado concediendo nada. Era inerte, y a un cambio de configuración de dejar de serlo. Un defecto real cuya gravedad no se parecía en nada a su apariencia, situado en la brecha entre las reglas de acceso que la gente lee en la interfaz y las reglas de acceso que la ruta de la solicitud consulta realmente.

La forma que comparten

Tres defectos, tres subsistemas, una forma. En cada caso el comportamiento degradado del sistema era indistinguible de su comportamiento correcto:

  • La vista previa servía una página. Solo que la revisión equivocada.
  • La comprobación del esquema pasaba. Comprobaba cada lado, no la costura.
  • La cuenta de servicio funcionaba. Sus privilegios declarados eran ficción.

Esto es lo que hace que los errores de costura sean caros de forma desproporcionada a su dificultad. Cada uno fue una corrección pequeña. Cada uno sobrevivió indefinidamente, porque sobrevivir es lo que hacen. Un sistema que se cae se arregla el martes. Un sistema que sirve discretamente lo equivocado se arregla cuando a alguien se le ocurre mirar de cerca, lo que puede ser nunca.

Por qué la canalización es estructuralmente ciega

Es tentador llamar a esto un problema de disciplina de pruebas. No lo es. Mire lo que cada capa mide realmente:

  • TypeScript comprueba la forma que usted afirma que tiene una respuesta. Las consultas son cadenas; tsc no tiene opinión sobre su contenido.
  • La compilación del front end compila código. Nunca contacta con el CMS.
  • Las pruebas unitarias simulan el cliente, así que verifican contra fixtures que eran ciertos cuando se escribieron.
  • Las pruebas del CMS pasan, porque desde la perspectiva del CMS nada está roto. Renombrar un campo es legítimo.
  • Las comprobaciones de salud confirman que el servicio responde. No confirman que responda correctamente.

Cada capa es honesta sobre algo adyacente a la pregunta. La costura no está representada en ningún artefacto, así que nada puede comprobarla. No se puede llegar a cubrir con pruebas algo que no existe en su repositorio.

La solución en los tres casos: convertir el invariante en un artefacto

El patrón que resolvió los tres fue el mismo. Tome el invariante que vivía en la cabeza de alguien y dele una representación física que algo pueda comprobar.

Un archivo de contrato. El CMS exporta su esquema GraphQL y lo confirmamos en el repositorio del front end: unos 97 KB, 136 tipos. Una prueba unitaria construye el esquema a partir de ese archivo, captura la consulta que enviaría cada función de obtención y la valida. La deriva incompatible ahora falla en el momento del pull request nombrando el campo exacto; la deriva aditiva aparece como un diff que una persona lee. Vive en el front end deliberadamente: el front end declara lo que necesita, y cualquier CMS que esté detrás de la costura debe satisfacerlo. Eso es lo que mantiene el contrato portable en lugar de con forma de Drupal.

Vale la pena decirlo claramente: no escribimos casi nada de esto. drush graphql:dump y drush graphql:detect-breaking-changes ya existen aguas arriba, y el segundo ya distingue lo incompatible de lo aditivo. También estuvimos a punto de abandonar el enfoque por una suposición errónea: que disable_introspection bloquearía la exportación del esquema. No lo hace. Esa bandera instala una regla de validación de consultas; imprimir el esquema recorre el mapa de tipos en el propio proceso y no ejecuta ninguna consulta. Compruebe primero la suposición que mataría el diseño.

Un resolutor que devuelve lo que autoriza. La vista previa usa ahora un campo dedicado que carga la revisión más reciente —prefiriendo la revisión más reciente que afecte a la traducción, para que un borrador en español no se resuelva en silencio a la copia de trabajo en inglés— y comprueba el acceso de visualización sobre el objeto de revisión cargado. No hay una rama de autorización separada que pueda desincronizarse de la lógica de carga. El objeto devuelto es el objeto autorizado.

Un alcance por función. Dos consumidores, dos alcances: uno que lee contenido publicado, otro que lee borradores. El cliente público no puede filtrar un borrador porque no tiene ningún alcance que pueda ver uno. Los secretos pasaron a configuración cifrada y obtenida del entorno en lugar de texto plano en la configuración exportada, lo que elimina todo un vector de recurrencia: la clase de rotura en la que las dos mitades discrepan sobre un secreto compartido y el handshake falla por razones que ningún registro explica.

La regla que vale la pena robar

Donde el modo degradado de un sistema es servir algo razonable, la ausencia de un error no demuestra nada. Las pruebas de aceptación en un sistema así deben escribirse para detectar un éxito, nunca la falta de una falla.

La nuestra para la vista previa es una sola frase: abra un borrador de un nodo ya publicado y confirme que ve sus ediciones no publicadas. No "confirme que la vista previa carga": eso pasa estando roto. No "confirme que no hay errores": nunca hubo ningún error. La única pregunta que discrimina es si los bytes en pantalla son los que aún no están en vivo.

Aprendimos la misma lección de la forma cara en el lado de la identidad. Nuestro primer plan de verificación decía "confirmar que la emisión del token sigue teniendo éxito". Una cuenta de servicio bloqueada sigue emitiendo tokens perfectamente; es cada solicitud posterior la que devuelve 401. El plan habría certificado una interrupción como saludable. Nunca verifique una credencial emitiendo un token; verifíquela gastando uno.

Ambos errores tienen la misma raíz: medir el paso anterior al que importa. La emisión en lugar del uso. El renderizado en lugar de la corrección. La medición es fácil de tomar, que es exactamente por lo que es la que uno toma.

Dónde nos deja esto

Estas tres correcciones son una sola capacidad con tres sombreros: un plano de gestión y un plano de entrega que ya no pueden distanciarse sin que algo lo diga. El contrato de esquema protege lo que cruza la costura. La identidad de máquina con alcance acotado protege quién puede cruzarla. Los secretos cifrados y obtenidos del entorno protegen el propio handshake. Nada de esto es exótico, y nada es específico de Drupal: el contrato se vincula a un modelo de contenido normalizado, no a los internos del CMS, que es la propiedad que permite que la misma protección se sitúe sobre un back end distinto.

El resumen honesto de la versión no es que arreglamos tres errores. Es que encontramos tres lugares donde el sistema nos había estado diciendo que todo iba bien, y reemplazamos cada uno por algo que puede decirnos cuando no es así.