El pasado 13 de Diciembre de 2022 relaté una crónica relacionada con mis primeras aproximaciones a Angular y los motivos que me llevaron a ello. En esa misma crónica mencioné que era la introducción para una siguiente anécdota: ésta.
Reitero lo dicho anteriormente, conforme avanzas en el conocimiento y la práctica, no solo ganas pericia, destreza y experiencia; problemas nuevos y conocimiento qué adquirir se hacen presentes.
Recapitulando, mi primera aplicación «funcional» fue una de tipo Windows cliente-servidor. Una ventaja de este tipo de aplicaciones Windows, sobre todo cuando están en una intranet es que la aplicación que funge como cliente, normalmente estará consumiendo servicios específicos que sólo requieren ser invocados o que se exponen como un servicio de la misma red, por ejemplo: un servicio de impresión, un servicio de mensajería, servicio de correo, servicio de base de datos.
En estos casos, el cliente se debe identificar (en caso de tener este tipo de control de acceso) ante el proveedor de servicio, y lo más común, es contar con una cuenta de usuario y contraseña que se proporcionan al proveedor del servicio que se encargará de validarlos y decidir si proporcionará el servicio en turno o no. Estas credenciales pueden ser controladas directamente por el servicio (con su propia lista de usuarios y contraseñas asociadas), la operación de validación puede ser delegada a otro servicio como por ejemplo Active Directory, puede ser controlada mediante mecanismos de hardware/software (con una tarjeta de acceso, por dirección MAC/RED) o por un certificado. El siguiente diagrama es una simplificación de este escenario.

La desventaja principal de este tipo de aplicaciones, es sin duda alguna la distribución. Cada vez que hay que actualizar o ajustar algo, se debe cambiar en cada uno de los clientes. Suena fácil si partimos del diagrama que precede a este párrafo porque solo hay 3 clientes, pero piensa por ejemplo, en instalar MS Office en un colegio o una empresa, y más, si poseen varias locaciones.
Instalar o actualizar software de este tipo implica ir a cada una de las computadoras a ejecutar el instalador y configurar. Si bien, con Active Directory y una correcta aplicación de despliegue por dominio se puede automatizar, no todo el mundo se puede permitir el costo de este software y tendrá que buscar alguna estrategia de despliegue que le sea favorable en tiempo y recursos.
Con las tecnologías web para servidor, este inconveniente de las aplicaciones de escritorio parecía llegar a su fin. En lugar de instalar el cliente del programa específico, ahora sólo había que asegurarse de tener un navegador web y tener acceso a la aplicación. El diagrama siguiente muestra un esquema simplificado de este tipo de aplicaciones:

En este tipo de despliegue, en lugar de que el cliente gestione los recursos con los distintos proveedores, ahora solicitan el servicio a un único proveedor, el servidor Web o servidor de aplicaciones, y este último es el que a su vez, para atender las peticiones recibidas, gestiona con los demás servidores o servicios los recursos necesarios. La incursión en este esquema para mí fue con PHP, Apache Web server y MySQL.
Hasta aquí todo parece que realmente este esquema es la panacea del software, más aún con tecnologías como HTML5 y Javascript que a la fecha de esta crónica (Diciembre 2022) nos permiten simular una experiencia prácticamente igual a la que se tiene en una aplicación de escritorio.

Si no podías confiar enteramente en una aplicación de escritorio, mucho menos en una aplicación web. Incontables situaciones de seguridad vulnerada e información filtrada se dieron. Esto derivó en que una aplicación web, personificada en el equipo cliente a través del navegador web, tuviera acceso limitado a los recursos del equipo. Por ejemplo, una aplicación web no puede escribir directamente datos en los dispositivos de almacenamiento del equipo cliente, ni tener acceso a sus periféricos de forma automática sin recibir permiso explícito del usuario.
Esta es la solicitud de acceso a la cámara web de un equipo laptop de una aplicación en línea usada para tomar fotografías del usuario:


Así podemos ver ejemplos con dispositivos de geolocalización, audio y video, micrófono, impresora, etc.
En el momento en que abres una aplicación web, sobre todo si es una en Internet tienes que estar seguro de que realmente estás interactuando con esa aplicación. Muchos fraudes se han hecho porque gente con conocimiento se aprovecha de gente que carece de este y hacen clones de aplicaciones para robar datos o dinero, y es ahí donde dejaremos esta ya larga introducción e historia para pasar al tema del cross-origin…
La primera vez que yo vi un mensaje de error relacionado fue cuando quise usar el tag <iframe> en una de mis aplicaciones para simular algo como una barra de menú y la aplicación en el extremo derecho. Este era el layout:

Mi idea era generar un layout estático que no cambiara de tamaño o posición. La barra de título y la barra de menú estarían fijas, la barra de menú mostraría botones que al ser presionados, desplegarían dentro de iframe la sección de la aplicación que yo quería.
El siguiente diagrama muestra un flujo funcional:

Cuando se hace clic en alguna de las opciones del menú, mediante el tag <a href=»…»> y con la cláusula target se envía el resultado al iframe para que se renderice ahí. En la siguiente captura de pantalla se puede apreciar un ejemplo de este funcionamiento:


El ejercicio iba bien hasta que quise mostrar algo que estaba fuera de mi servidor. Por ejemplo, si quisiera mostrar la página de Google dentro del iframe, obtengo esto:


¿Google rechazó la conexión? ¿Por qué en el iframe tiene este comportamiento pero cuando pones la misma URL en el navegador, fuera de un iframe, sí tienes una respuesta? Realmente mi intención no era mostrar Google en un frame, así que desistí de esa prueba y continué con mi experimento. Al final, no procedí así porque el iframe lleva un contexto por su cuenta, separado de la página principal, por lo que, una recarga de la página principal me hacía perder el avance en el iframe, y por su parte una recarga en el iframe igualmente causaba inconsistencias en el flujo.
Trasladémonos años desde mi experimento hasta la aplicación que estoy haciendo en Angular. Después de modelar varias vistas, llegó el momento de hacer interactuar el front-end hecho en Angular con el back-end hecho en PHP, retomando lo que ya tenía hecho con PHP solo. Lo primero fue hacer que la pantalla de login validara las credenciales del usuario y según la respuesta, evaluar si redirigir al usuario a la vista principal de la aplicación o mostrarle un error de inicio de sesión.
Esta parte fue relativamente sencilla, generé un servicio al que se le envían las credenciales del usuario firmándose, ese servicio hace una petición HTTP con su correspondiente observable, se envía la petición, se espera y evalúa la respuesta, y listo.
Después pasé al registro de un vendedor y aquí fue donde empecé con los problemas, particularmente con ese asunto del Cross-origin, que no quise revisar (y entender) cuando hice los experimentos con el iframe.
Esto fue lo ocurrido…
Generé un servicio que manda los datos del nuevo vendedor a una URL en PHP, que a su vez los recibe en formato json, transformándolo en objetos, que son pasados como parámetros a un modelo que los usa para hacer las inserciones en base de datos, para luego empezar a responder en la pila de llamadas hasta llegar al cliente http de Angular. Nada nuevo bajo el sol.
No tenía intenciones de complicarme, así que el código del servicio era una copia del servicio (de Angular) con el que se validan las credenciales. Al momento de hacer una prueba empezaron los errores:
Error al insertar en BD: la columna nombre no puede ir vacía.

Empiezo a revisar los logs en PHP y en efecto, el insert creado va con el nombre vacío, pero no es sólo eso, todo está vacío, excepto los campos que desde código se asignan, como la fecha y hora de inserción o el autonumérico que hace de llave primaria.
Abrí las herramientas de desarrollo en Edge (sí, uso MS Edge y me gusta) y de Chrome (probé en ambos) y veo en cada prueba que el objeto se está enviando correctamente en el payload.
Confundido, regresé al script de PHP y comencé a llenarlo de var_dump() y die() buscando en qué momento se perdían los datos.
No me lo van a creer, pero, todos los datos estaban presentes excepto en el momento en que yo invocaba el insert. Al habilitar el insert, de la nada, todos los datos que antes estaban ahora ya estaban nulos.
Observé que utilicé el nombre de variable $data en muchos lugares incluyendo el objeto json que se recibía y pensé «quizás está entrando en conflictos», así que renombré todo. No funcionó.
Reinicié el servidor web y el equipo. No funcionó.
Cambié la forma en como se hacía el insert. No funcionó.
En todos y cada uno de los casos, mientras no invocara insert, los datos estaban ahí. Activando esa sola instrucción todo se descomponía en cada prueba, el 100% de las veces.
Numerosas pruebas y cambios hice durante 2 días, alrededor de 8 horas. Imagina mi frustración al no poder encontrar solución. Pensé que todo el trabajo hecho previamente en PHP, que yo asumía reutilizable en un 80% realmente no me iba a servir de nada. El back-end está hecho con Codeigniter, y con estos resultados (fruto de la ignorancia) ya estaba considerando dejarlo y hacer todo en PHP nativo, incluso cambiar de tecnología.
Como todo esto me parecía inverosímil, llamé a mi testigo por excelencia, un gran amigo a quien, para mantener su anonimato pero para no encasillarlo en una etiqueta impersonal y genérica como «mi amigo» llamaremos ‘Diego‘.
He estado trabajando con Diego en varios proyectos, profesionales y personales; ese día me saludó, le conté lo que pasaba y tampoco lo creía. Así que en segundos estábamos en una sesión remota. Tal era el morbo de Diego que no le importó tomar la sesión en su celular mientras se transportaba a una cena.
Le conté lo que ya leyeron, todo lo que hice y para que no fuera un acto de fé, le mostré paso a paso lo que había hecho. El front-end, el PHP, el código, los logs, los var_dump() y print_r(), los datos. Incluso le mostré cómo activar el insert causaba los errores. Él conoce también Codeigniter, por lo que me sugirió cambiar la forma de inserción de los datos. Lo hicimos en vivo y nada el resultado fue lo mismo.
De pronto, él sugirió cambiar de cliente.
-¿Cómo cambiar de cliente? -pregunté.
-sí, o sea, que consumas el PHP de otro lado.
Abrí SOAP-UI para hacer una prueba como he hecho en otros servicios, pero había decidido no hacerlo con este. Tomé el JSON del log del front-end y lo mandamos en SOAP-UI.

Todo funcionó PERFECTAMENTE. La voz de Diego fue victoria al decir:
-¡Entonces no es PHP lo que está mal!
-Así parece -dije yo.
Ustedes no lo saben, pero antes de ese momento de claridad, yo estaba odiando PHP y dándole la razón a toda la gente que lo critica. Pero al ver la luz, mi coraje se fue y regresé a PHP al podio de mis lenguajes de programación.
Diego ya estaba teniendo (más bien fingiendo, yo diría) tener problemas de comunicación porque según él «venía en carretera». Así que nos despedimos con la promesa de contarle cómo se habría resuelto, en caso de ser así.
Bueno, PHP no está mal, aquí termina la crónica, ¿no?

No. Los cuestionamientos llegaron en un soliloquio:
¿Por qué el login sí funcionó? Es el mismo tipo de cliente http, hacia el mismo end-point hecho con la misma tecnología.
¿Qué tenían diferente?
Inicialmente pensé que era que el servicio de login solo consultaba, pero no, inserta e incluso actualiza datos.
Las instrucciones de registro de vendedor funcionan correctamente cuanto todo ejecuta desde PHP (en la aplicación sin Angular). Los datos se envían correctamente desde Angular. ¿Cuál podría ser el problema?
Decidí buscar a mi hermano, que también se dedica a estos chismes y quien de hecho tiene mucho más tiempo y pericia usando Angular.
Le mostré todo lo que tenía, lo que había hecho y lo que había observado. Encontró que no estaba enviando opciones y headers a la petición hacia PHP. Los agregamos y probamos de nuevo.
Nada, el comportamiento era el mismo.
Nuevamente regresamos a las herramientas de desarrollador del navegador y vimos esto:

Había 2 peticiones hacia el endpoint, la que aparece en texto rojo es la que marca el error por datos vacíos (flecha vertical), y una segunda (flecha horizontal) que está respondida con un 200 OK, pero que envía el método OPTIONS.
¿Por qué Angular envía OPTIONS si es un POST? Observa el código enseguida:

La respuesta era que Angular no estaba haciendo esa petición extra, sino el navegador web, como se explica en este artículo:
Entonces, ¿qué fue lo que sucedió?
El servidor web está detectando que la aplicación está intentando conectar con una URL que es diferente a la que está dando el servicio en ese momento, por lo que detecta un Cross-Origin, actuando como salvaguarda del usuario (recuerda el asunto de las aplicaciones fraude).
Para corroborar que la aplicación a la que se intenta contactar acepta peticiones de otros orígenes (el servicio es quien debe asegurar que sus clientes son válidos), el navegador web envía una señal de prevuelto (pre-flight) vacía pero con los headers relacionados a cross-origin (CORS):

Mi back-end en PHP no le estaba contestando que recibiría peticiones de terceros, como se expresa en este header (ahora ya lo tiene):

Si el navegador web envía una señal de prevuelo que no se le contesta de forma correcta, ya no realiza la petición que en realidad pretendía hacer, esto es justo lo que pasaba con mi aplicación, la señal de prevuelo no se contestaba apropiadamente.
Entonces…
Yo estaba recibiendo errores de PHP de intentar insertar datos vacíos porque la señal de prevuelo era la que llegaba primero y como iba en blanco (y yo no validé esta situación) se generaba ese error de insert. Como la señal de prevuelo no se contestaba correctamente, la llamada con los datos se encolaba pero nunca se realizaba, por eso yo veía los datos pero no una invocación.
Para solventarlo, tenía que hacer que PHP identificara si la petición llegaba de un prevuelvo con OPTIONS, esta es la solución que encontré en el foro de Stackoverflow citada anteriormente:

Y esto es básicamente la corrección. Al contestar correctamente el prevuelo, la aplicación Angular después envía los datos y la inserción funciona correctamente.

Pero a ver, ¿por qué el login sí funcionaba sin tener eso anteriormente? Porque la señal de prevuelo recibe una respuesta de usuario inexistente (ese sí valida que esté vacío). Aunque el mensaje sea que el usuario no existe o que faltan datos, PHP responde con un HTTP 200 y eso es interpretado por el navegador web para que continue con el flujo; en el caso del insert, estaba respondiendo un HTTP 501 (error en server), por lo que el navegador web ya no continuó con la petición.
¡Y ahora sí, todo hace sentido!
Ten mucho cuidado con ese CROSS-ORIGIN (CORS) te puede sacar canas y hacer perder valiosas horas de trabajo revisando falsos errores.
Deja un comentario