Saltar al contenido
Development

Un 500 escondido dentro de las rutas aisladas de un engine montado

Por Victor Da Luz
railsrubydev-logblog-manager

MissionControl::Jobs es la interfaz web que uso para ver correr los jobs de Solid Queue. Está montada en /jobs y queda detrás del mismo login que el resto de blog-manager. Excepto que no era así: visitarla sin sesión iniciada daba un 500, no una página de login. El tipo de bug invisible a menos que lo busques, porque cualquiera que ya haya iniciado sesión nunca lo ve.

Qué construí

Una línea, al final: redirect_to new_session_path pasó a ser redirect_to main_app.new_session_path en el concern de autenticación compartido. Pero llegar a esa línea significó entender algo sobre los engines de Rails que no tenía interiorizado antes.

Lo que me sorprendió

MissionControl::Jobs se monta como un engine con su propio namespace de rutas aislado. Está configurado para heredar el ApplicationController de mi app, así que recibe gratis mi lógica de autenticación, login requerido, igual que en todas partes. Lo que no entendía: cuando ese código de autenticación compartido corre dentro de una solicitud al engine, llamar a un route helper sin calificar como new_session_path no se resuelve contra las rutas de mi app. Se resuelve contra las rutas propias del engine. Y el engine no tiene una acción sessions#new, así que en vez de una redirección obtienes UrlGenerationError, un 500, justo a través de la capa de autenticación que se supone debía mantener a la gente afuera.

El fix, main_app.new_session_path, es el modismo estándar para decir “no, me refiero a las rutas reales de la aplicación, no al contexto en el que resulte estar ejecutándose esto.” Una vez que entendí el mecanismo, era obvio. Antes de eso, el mensaje de error (No route matches {:action=>"new", :controller=>"sessions", :server_id=>nil}) simplemente parecía sin sentido, ¿por qué necesitaría un server_id la sesión?

Escribí la prueba antes del fix, específicamente para tener una falla real y reproducida contra la cual comparar, en vez de confiar en mi propio diagnóstico. Menos mal, porque la prueba misma terminó topándose con la misma clase de bug en otro lugar. Después de que la prueba visita /jobs, llamar al helper de la ruta de login desde la prueba, incluso calificado con main_app., se resuelve contra el contexto de ruteo que dejó el engine y verifica lo que no debía. Las pruebas de integración de Rails cargan una noción de “la página actual” entre solicitudes dentro de una misma prueba, y una vez que visitaste una ruta montada en un engine, ese contexto se queda ahí para las llamadas a los helpers propios de la prueba. El fix ahí fue distinto: capturar la ruta esperada antes de hacer la solicitud, no después.

La revisión de código encontró algo más que se me había pasado: el mismo concern compartido tiene un segundo route helper sin calificar dos líneas más abajo, en el código que decide a dónde se manda después de un login exitoso. Actualmente inalcanzable desde el engine, nada lo llama desde ahí hoy, pero es exactamente la misma forma de bug sentada en el mismo archivo, solo que dormida. También lo arreglé, ya que estaba ahí y ya había demostrado que el fix no cambia el comportamiento en ningún otro lado.

Una cosa que deliberadamente no arreglé: revisar ese mismo código de redirección de login sacó a la luz un problema real y separado, guarda cualquier URL que se estuviera intentando alcanzar cuando expiró la sesión, incluyendo el método, pero siempre redirige de vuelta con GET después de volver a iniciar sesión. Si esa URL guardada era una acción exclusiva de POST, el resultado era un 404 al volver en vez de llegar al destino esperado. Revisé el historial de git y confirmé que esto es completamente anterior a hoy, es el comportamiento original del scaffold de Rails 8, no algo que yo introduje. Es real, pero arreglarlo bien significa decidir cómo debería comportarse la app en ese caso, no agregar una línea más a un PR que se suponía era un fix puntual para otra cosa. Lo archivé como issue separado en su lugar.

Qué sigue

Verifiqué de punta a punta en vez de confiar solo en la suite de pruebas: confirmé que el bug estaba activo en producción (le hice curl, obtuve el 500 yo mismo), desplegué el fix, le hice curl otra vez y obtuve la redirección que esperaba. Algo pequeño, pero “las pruebas pasan” y “lo que estaba realmente roto frente a usuarios reales ya está arreglado” no son la misma afirmación, y es fácil confundirlas.

Lecturas relacionadas

Development

El botón de commit del editor es un botón de deploy

Confirmar un borrador a main despliega el blog automáticamente. En cuanto eso quedó claro, sync vs. async dejó de ser una cuestión de estilo, más el caso especial de afiliado heredado que un validador nuevo casi rompió.

Leer