Cloudflare R2 Image URL Display Issue: Detailed Explanation and Fix

Problem Description

We’ve recently noticed an issue with image display on the forum, specifically:

  • Image thumbnails are not displaying correctly.
  • Clicking on the images displays the full-size image correctly.
  • The browser’s developer tools show incorrect image URLs.

After investigation, the root cause is an incorrect domain name in the image URLs:

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

The system is using the raw R2 bucket wrong domain instead of our configured CDN domain.

Technical Analysis

This is a known issue with Discourse when handling images stored in Cloudflare R2. In some cases, even when s3_cdn_url is configured, Discourse might still use the raw storage URL instead of the CDN URL when generating optimized images (like thumbnails).

This could be related to the following factors:

  • Discourse version
  • The configuration of the S3-compatible storage
  • How URLs are stored in the OptimizedImage table

Solution

The simplest and most effective solution is to use a Discourse theme component for client-side repair. This doesn’t require any database operations or server configuration changes. It only involves adding a short JavaScript code snippet that automatically replaces the incorrect URLs with the correct ones in the browser.

Theme Component Code

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

export default apiInitializer("0.11.1", (api) => {
  // Fix already loaded images
  function fixImageUrls() {
    const badDomain = "info.7a4081a2d83d3f43fe6b1be1c926fd1c.r2.cloudflarestorage.com";
    const goodDomain = "store.starorigin.cc";

    // Fix regular images
    document.querySelectorAll(`img[src*="${badDomain}"]`).forEach(img => {
      img.src = img.src.replace(badDomain, goodDomain);
    });

    // Fix lazy-loaded images
    document.querySelectorAll(`img[data-src*="${badDomain}"]`).forEach(img => {
      img.setAttribute('data-src', img.getAttribute('data-src').replace(badDomain, goodDomain));
    });

    // Fix background images
    document.querySelectorAll('[style*="background"]').forEach(el => {
      if (el.style.backgroundImage && el.style.backgroundImage.includes(badDomain)) {
        el.style.backgroundImage = el.style.backgroundImage.replace(badDomain, goodDomain);
      }
    });

    // Fix various other potential attributes
    ['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));
      });
    });
  }

  // Fix images in the editor
  api.decorateCooked($elem => {
    fixImageUrls();
  }, { id: 'fix-r2-image-urls' });

  // Fix after initial load
  api.onPageChange(() => {
    fixImageUrls();
  });

  // Handle dynamically loaded content
  const observer = new MutationObserver(mutations => {
    fixImageUrls();
  });

  // Start observing after the DOM is loaded
  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']
    });
  }
});

How the Code Works

This code performs the following actions:

  1. Comprehensive Detection: Finds all image URLs containing the incorrect domain.
  2. Multiple Element Handling: Handles various image elements and attributes (img tags, lazy-loaded images, background images, etc.).
  3. Dynamic Monitoring: Uses a MutationObserver to monitor page changes, ensuring dynamically loaded content is also fixed.
  4. Discourse Integration: Integrates with the Discourse API to handle various special scenarios.

Installation Steps

  1. Log in to your Discourse administrator account.
  2. Go to Admin > Customize > Theme Components.
  3. Click the New button.
  4. Select the Create new component option.
  5. Name it “Fix R2 Image URLs” (or any name you prefer).
  6. In the “Javascript” tab, paste the code above.
  7. Click the Create button.
  8. Click the Enable button and choose the theme to apply it to (usually “Default”).

Verification

After installation:

  1. Refresh the forum page.
  2. View posts containing images.
  3. Confirm that thumbnails are displayed correctly.
  4. Use your browser’s developer tools to check that image requests are all pointing to the CDN domain.

While a client-side solution is the simplest, fastest, and lowest-risk approach, especially when direct server access is limited.

Conclusion

This simple theme component effectively addresses the image URL issue when integrating Discourse with Cloudflare R2 storage, without requiring server changes or complex configurations. Although it fixes the problem on the client-side rather than addressing the root cause, it’s easy to implement, provides immediate results, and is an ideal solution.

If your Discourse site is also experiencing similar issues, feel free to try this solution.

2 Likes

does this issue only impact chat? trying to understand scope.

Thank you for your very helpful report here!

1 Like

Yes, this bug only affects the chat. My S3 service provider is Cloudflare R2. When I send an image in the chat interface, the image cannot use the default CDN link settings, resulting in the failure to load the image.

1 Like