Un 500 escondido dentro de las rutas aisladas de un engine montado
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
Qué pasa cuando un job transmite y nadie escucha
Cerrando el ciclo de la imagen destacada: frontmatter que solo inserta, una validación que detectó drift real, y un broadcast sin oyentes.
Una corrección de desfase de documentación que no fue tan aburrida como sonaba
Tres puntos de auditoría que se volvieron algo más: una afirmación a medio corregir, un restablecimiento de contraseña muerto en silencio, y un correo de staging que enlazaba a producción.
Borrando código muerto, y descubriendo una razón equivocada para una respuesta correcta
Un issue de limpieza con una justificación equivocada, un barrido de documentación que no hacía falta, y la credencial huérfana que atrapó una revisión.
También te podría ser útil
NordPass
Gestor de contraseñas del equipo detrás de NordVPN, con un plan gratuito.
Como afiliado de NordPass, obtengo ingresos por las compras que califican.
Más informaciónProton Drive
Almacenamiento en la nube cifrado, del equipo detrás de Proton Mail.
Como socio de Proton, obtengo ingresos por las compras que califican de los servicios de privacidad y seguridad de Proton (Pass, Mail, VPN, Drive).
Más informaciónRackNerd VPS
Alojamiento VPS económico para servicios ligeros que funcionan de forma continua.
Como afiliado de RackNerd, obtengo ingresos por las compras que califican.
Más información