Agregar Unsplash como segundo proveedor de imágenes hero
blog-manager ya tenía una fuente de imágenes hero: Pexels. Un cuadro de búsqueda llama a la API de Pexels, se elige una foto, y la URL queda enlazada directamente (hotlink) en el frontmatter del post. Un issue anterior convirtió eso en una capa agnóstica de proveedor (un registro indexado por nombre de proveedor, resultados intercalados en round-robin, una forma de hash normalizada compartida). Este issue fue sobre conectar de verdad un segundo proveedor a ese registro: Unsplash.
Lo interesante no fue la integración de búsqueda en sí. Las pautas de la API de Unsplash tienen dos requisitos que Pexels no tiene: hay que usar hotlink (nada de descargar y volver a alojar, lo cual coincide convenientemente con cómo esta app ya trata a Pexels), y hay que hacer ping a un endpoint de “seguimiento de descargas” por foto cada vez que alguien elige una foto, solo para que Unsplash pueda contarlo como una descarga en sus propias métricas.
Lo que construí
Unsplash::Client replica a Pexels::Client casi línea por línea: Net::HTTP hecho a mano, un proc connection: como punto de prueba en vez de traer webmock o VCR, la misma jerarquía Error/AuthError bajo ImageSearch::Error. La única diferencia real es la forma de la respuesta de Unsplash (results en vez del photos de Pexels, urls.regular/urls.small en vez de src.large/src.medium) y un nuevo método track_download para el ping de seguimiento.
El ping en sí se convirtió en su propio job pequeño, UnsplashDownloadPingJob, disparado desde select_image en el momento en que alguien elige una foto de Unsplash. Sin reintentos, sin discard_on, solo un rescue-and-log. Si el endpoint de seguimiento de Unsplash está caído, eso no es motivo para bloquear a alguien que quiere elegir una imagen hero.
Una decisión que casi me salté
La URL de seguimiento de descargas (links.download_location) viene en la respuesta de búsqueda de Unsplash y viaja como un campo de formulario oculto a través del botón “Select” ya existente en cada resultado de búsqueda. Lo cual significa que, por construcción, es un valor que controla el navegador. Mi primer intento simplemente tomaba lo que fuera que enviara el cliente y disparaba un GET autenticado con la clave de Unsplash de la app adjunta. Un campo de formulario manipulado filtraría con gusto esa clave a cualquier host que un atacante pusiera en ese campo.
Brakeman no lo detectó, porque no es un patrón que revise, y es fácil pasarlo por alto porque url_full (la URL real de la imagen) tiene exactamente la misma forma de “confiar en el cliente” pero es inofensiva, ya que esa la busca el navegador, no el servidor. La solución fue una verificación de host de una línea en track_download antes de adjuntar el header Client-ID. Algo pequeño, pero del tipo de cosas obvias en retrospectiva e invisibles hasta que uno se pone a buscarlas.
Lo que me sorprendió
Verificar esto manualmente sin una clave real de Unsplash (registrar una es un trámite en el dashboard para más adelante) se convirtió en un pequeño yak-shave. Terminé manejando la app a través de ActionDispatch::Integration::Session dentro de un script de rails runner contra la base de datos real de desarrollo, ya que la app está detrás de autenticación OIDC real y no hay un bypass de modo desarrollo. Eso funcionó, hasta que mezclé un dispatch completo de request/response con una consulta simple de ActiveRecord en el mismo proceso y apareció un error genuinamente confuso de ActiveSupport::ExecutionContext en las profundidades de Rails. Nada que ver con mi código. Separar cada verificación en su propia invocación de runner lo hizo desaparecer. No me había topado antes con esa interacción en particular.
Lo único que funcionó sin problemas: la app ya tenía una clave real de Pexels configurada en desarrollo, así que pude confirmar que el cambio de registro de dos proveedores no rompió la búsqueda existente de solo Pexels. Devolvió resultados reales de Pexels y el pie de página “Photos provided by Pexels”, exactamente como antes.
Qué sigue
Esto se publica con Unsplash conectado a la búsqueda, la selección, la atribución y la página de configuración, pero no queda activo hasta que se registre una Unsplash Access Key y se pegue en Settings. El modo demo es de 50 solicitudes por hora, algo que el caché de búsqueda de 12 horas ya existente vuelve suficiente para una sola persona. La aprobación de producción (5.000 por hora) necesita capturas de pantalla de atribución enviadas a Unsplash, después de que esto se haya usado de verdad por un tiempo.
Lecturas relacionadas
Agregando Openverse: el tercer proveedor de imágenes, y el primero que nadie tiene que configurar
Más de 800 millones de imágenes CC y de dominio público, anónimo por defecto, y la consecuencia de diseño de que una instalación sin ningún proveedor configurado ahora hace llamadas reales a la API.
El error de la imagen hero que en realidad eran tres errores
Una tarjeta que mentía diciendo que un post no tenía imagen hero, una ruta relativa resuelta contra el origen equivocado, y un 403 de hotlinking basado en Referer de mi propio blog contra mi propia app.
Construir imágenes de portada autoalojadas antes de tener algo que las necesitara
Un pipeline de descarga y realojamiento para imágenes con licencia CC: vips desde un buffer, una conversión forzada a UTF-8 que habría corrompido los bytes del JPEG, y una cola desactualizada que hizo parecer un error a una protección que funcionaba bien.