Problema de visualización de URL de imagen de Cloudflare R2: explicación detallada y solución

Descripción del problema

Recientemente hemos notado un problema con la visualización de imágenes en el foro, específicamente:

  • Las miniaturas de las imágenes no se muestran correctamente.
  • Al hacer clic en las imágenes, se muestra la imagen a tamaño completo correctamente.
  • Las herramientas de desarrollador del navegador muestran URL de imágenes incorrectas.

Después de la investigación, la causa raíz es un nombre de dominio incorrecto en las URL de las imágenes:

  • URL correcta: https://store.starorigin.cc/optimized/1X/[imageID].jpeg
  • URL incorrecta: https://info.7a4081a2d83d3f43fe6b1be1c926fd1c.r2.cloudflarestorage.com/optimized/1X/[imageID].jpeg

El sistema está utilizando el dominio incorrecto del bucket R2 en bruto en lugar de nuestro dominio CDN configurado.

Análisis técnico

Este es un problema conocido con Discourse al manejar imágenes almacenadas en Cloudflare R2. En algunos casos, incluso cuando s3_cdn_url está configurado, Discourse podría seguir utilizando la URL de almacenamiento en bruto en lugar de la URL CDN al generar imágenes optimizadas (como miniaturas).

Esto podría estar relacionado con los siguientes factores:

  • Versión de Discourse
  • La configuración del almacenamiento compatible con S3
  • Cómo se almacenan las URL en la tabla OptimizedImage

Solución

La solución más sencilla y eficaz es utilizar un componente temático de Discourse para la reparación del lado del cliente. Esto no requiere ninguna operación de base de datos ni cambios en la configuración del servidor. Solo implica agregar un pequeño fragmento de código JavaScript que reemplaza automáticamente las URL incorrectas por las correctas en el navegador.

Código del componente temático

import { apiInitializer } from "discourse/lib/api";

export default apiInitializer("0.11.1", (api) => {
  // Corregir imágenes ya cargadas
  function fixImageUrls() {
    const badDomain = "info.7a4081a2d83d3f43fe6b1be1c926fd1c.r2.cloudflarestorage.com";
    const goodDomain = "store.starorigin.cc";

    // Corregir imágenes normales
    document.querySelectorAll(`img[src*="${badDomain}"]`).forEach(img => {
      img.src = img.src.replace(badDomain, goodDomain);
    });

    // Corregir imágenes de carga diferida
    document.querySelectorAll(`img[data-src*="${badDomain}"]`).forEach(img => {
      img.setAttribute('data-src', img.getAttribute('data-src').replace(badDomain, goodDomain));
    });

    // Corregir imágenes de fondo
    document.querySelectorAll('[style*="background"]').forEach(el => {
      if (el.style.backgroundImage && el.style.backgroundImage.includes(badDomain)) {
        el.style.backgroundImage = el.style.backgroundImage.replace(badDomain, goodDomain);
      }
    });

    // Corregir varios otros atributos potenciales
    ['srcset', 'data-large-src', 'data-small-src', 'data-download-href'].forEach(attr => {
      document.querySelectorAll(`[${attr}*="${badDomain}"]`).forEach(el => {
        el.setAttribute(attr, el.getAttribute(attr).replace(badDomain, goodDomain));
      });
    });
  }

  // Corregir imágenes en el editor
  api.decorateCooked($elem => {
    fixImageUrls();
  }, { id: 'fix-r2-image-urls' });

  // Corregir después de la carga inicial
  api.onPageChange(() => {
    fixImageUrls();
  });

  // Manejar contenido cargado dinámicamente
  const observer = new MutationObserver(mutations => {
    fixImageUrls();
  });

  // Comenzar a observar después de que el DOM esté cargado
  if (document.readyState === "loading") {
    document.addEventListener('DOMContentLoaded', () => {
      fixImageUrls();
      startObserver();
    });
  } else {
    fixImageUrls();
    startObserver();
  }

  function startObserver() {
    observer.observe(document.body, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ['src', 'data-src', 'srcset', 'style']
    });
  }
});

Cómo funciona el código

Este código realiza las siguientes acciones:

  1. Detección integral: Encuentra todas las URL de imágenes que contienen el dominio incorrecto.
  2. Manejo de múltiples elementos: Maneja varios elementos y atributos de imágenes (etiquetas img, imágenes de carga diferida, imágenes de fondo, etc.).
  3. Monitoreo dinámico: Utiliza un MutationObserver para monitorear los cambios en la página, asegurando que el contenido cargado dinámicamente también se corrija.
  4. Integración con Discourse: Se integra con la API de Discourse para manejar varios escenarios especiales.

Pasos de instalación

  1. Inicie sesión en su cuenta de administrador de Discourse.
  2. Vaya a Admin > Personalizar > Componentes temáticos.
  3. Haga clic en el botón Nuevo.
  4. Seleccione la opción Crear nuevo componente.
  5. Nómbrelo “Corregir URL de imágenes R2” (o el nombre que prefiera).
  6. En la pestaña “Javascript”, pegue el código anterior.
  7. Haga clic en el botón Crear.
  8. Haga clic en el botón Habilitar y elija el tema al que aplicarlo (generalmente “Predeterminado”).

Verificación

Después de la instalación:

  1. Actualice la página del foro.
  2. Vea las publicaciones que contienen imágenes.
  3. Confirme que las miniaturas se muestran correctamente.
  4. Utilice las herramientas de desarrollador de su navegador para verificar que todas las solicitudes de imágenes apunten al dominio CDN.

Si bien una solución del lado del cliente es el enfoque más simple, rápido y de menor riesgo, especialmente cuando el acceso directo al servidor es limitado.

Conclusión

Este sencillo componente temático aborda eficazmente el problema de las URL de imágenes al integrar Discourse con el almacenamiento Cloudflare R2, sin necesidad de cambios en el servidor o configuraciones complejas. Aunque soluciona el problema en el lado del cliente en lugar de abordar la causa raíz, es fácil de implementar, proporciona resultados inmediatos y es una solución ideal.

Si su sitio de Discourse también está experimentando problemas similares, no dude en probar esta solución.

5 Me gusta

¿este problema solo afecta al chat? intentando comprender el alcance.

¡Gracias por tu informe tan útil aquí!

1 me gusta

Sí, este error solo afecta al chat. Mi proveedor de servicios S3 es Cloudflare R2. Cuando envío una imagen en la interfaz de chat, la imagen no puede usar la configuración predeterminada del enlace CDN, lo que provoca que la imagen no se cargue.

1 me gusta

Parece que sigue siendo el mismo caso que en Upload images in chat can't be show normally when use s3 CDN, y se intentó una corrección que luego se revirtió.

He logrado solucionar esto con un componente de tema actualizado similar al anterior.

El inicializador debería verse así:

import { apiInitializer } from "discourse/lib/api";

export default apiInitializer("1.8.0", (api) => {

  // ⚠️ ¡Asegúrate de que estos sean tus dominios exactos!
  // ¡Revisa las rutas de las imágenes para la miniatura de publicaciones y de chat y compáralas!

  const badDomain = "yoursite-uploads.xxx.r2.cloudflarestorage.com";
  const goodDomain = "uploads.yoursite.com";

  function fixImage(img) {
    if (img.src && img.src.includes(badDomain)) {
      img.src = img.src.replace(badDomain, goodDomain);
    }
    if (img.dataset?.src && img.dataset.src.includes(badDomain)) {
      img.dataset.src = img.dataset.src.replace(badDomain, goodDomain);
    }
  }

  // 1. Corregir de forma segura las imágenes renderizadas en publicaciones/HTML estándar
  api.decorateCooked($elem => {
    const container = $elem[0];
    if (!container) return;
    
    container.querySelectorAll(`img[src*="${badDomain}"]`).forEach(fixImage);
  }, { id: 'fix-r2-chat-bug' });

  // 2. Observador de alto rendimiento estrictamente para elementos de Chat inyectados dinámicamente
  const observer = new MutationObserver(mutations => {
    for (const mutation of mutations) {
      
      // Si se inyecta un nuevo mensaje o elemento de chat en el DOM
      if (mutation.type === 'childList') {
        for (const node of mutation.addedNodes) {
          if (node.nodeType === 1) { // ELEMENT_NODE
            if (node.tagName === 'IMG') fixImage(node);
            
            // Buscar solo DENTRO del nuevo nodo, no en todo el documento
            node.querySelectorAll(`img[src*="${badDomain}"]`).forEach(fixImage);
          }
        }
      } 
      // Si cambia el atributo src de una imagen existente (carga diferida)
      else if (mutation.type === 'attributes') {
        if (mutation.target.tagName === 'IMG') {
          fixImage(mutation.target);
        }
      }
    }
  });

  // Iniciar el observador inmediatamente
  observer.observe(document.body, {
    childList: true,
    subtree: true,
    attributes: true,
    attributeFilter: ['src', 'data-src']
  });
});
3 Me gusta

Parece que este no es un problema que solo ocurre en el chat. Acabo de migrar un foro a R2. Las imágenes nuevas cargadas tienen todas la ruta correcta, la CDN de S3, pero las antiguas tienen la dirección del endpoint. Y para mí, solo se muestran si instalo el TC de @Lilly.

Aquí hay algunas muestras:


1 me gusta

oh, para imágenes antiguas (no un foro completamente nuevo) necesitas ejecutar la tarea rake:

cd /var/discourse
./launcher enter app
rake uploads:migrate_to_s3

luego vuelve a procesar las cargas locales:

rake posts:rebake_match["/uploads/default/original/"]

gracias por recordármelo, lo añadiré a mi documentación sobre almacenamiento de objetos R2

2 Me gusta

Sí, ya lo intenté, pero sigue siendo la URL del punto de acceso, así:
(https://bucketname.xxxx.r2.cloudflarestorage.com/original/1X/ac9e68ba07090c0d2d8fd3e38338e75e1e328448.jpeg)

1 me gusta

¿Añadiste esta línea a tu app.yml (o agregaste el dominio a la configuración de administrador S3 CDN URL)?

DISCOURSE_S3_CDN_URL: https://your.R2.domain.com # (tu dominio personalizado real de R2)

Todo debe estar en el orden correcto de operaciones también.

  1. Configuraciones de app.yml en admin
  2. rake migrate
  3. rebake posts

También existe la herramienta de reemplazo de cadenas que puede ayudar: ejecútala dentro del contenedor (cambia las cadenas por las tuyas específicas):

discourse remap "https://<cloudflare-account-id>.r2.cloudflarestorage.com/<R2-bucket-name>" "https://your.cdn.domain.com"
2 Me gusta

Sí, todo parece correcto y el remapeo no lo soluciona. Las miniaturas antiguas siguen rotas, pero la imagen aparece si hago clic en ella o si habilito el tc.

1 me gusta

Lo siento, que eso no te funcionó.

Tengo un PR para solucionar el error aquí:

2 Me gusta