Cloudflare R2 Image URL Display Issue: Detailed Explanation and Fix

AI-generated summary

Users are discussing a bug where images in Discourse chat (and potentially older forum posts) display using raw Cloudflare R2 bucket URLs instead of the configured S3 CDN domain, causing thumbnails to fail while full-size images load correctly.

qingfeng1024 initially reported the issue, noting that the system uses the raw R2 domain (*.r2.cloudflarestorage.com) instead of the custom CDN domain. They provided a JavaScript theme component to fix URLs client-side by replacing the bad domain with the good one via MutationObserver and API decorators. sam asked if the issue was limited to chat, which qingfeng1024 confirmed.

DevTeVe linked to a similar previous thread. Lilly shared an updated, configurable theme component that supports any S3-compatible storage and provided a detailed code snippet for manual patching. David_Ghost noted that the issue also affects old forum images after migrating to R2, even if new uploads work. Lilly advised running rake uploads:migrate_to_s3 and rake posts:rebake_match to fix old images, along with ensuring DISCOURSE_S3_CDN_URL is set in app.yml.

However, David_Ghost reported that the rake tasks and the discourse remap command did not resolve the issue for their old thumbnails. Lilly acknowledged the persistence of the bug and shared a pull request aimed at fixing the root cause in the Discourse codebase, where chat thumbnails bypass the S3 CDN URL setting to use raw S3 bucket URLs.

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.

5 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

Seems to still be the like Upload images in chat can't be show normally when use s3 CDN case, and a fix was attempted but later reverted

i have managed to fix this with an updated theme component that has settings for the domains. thus anyone can use it without having to hard code their own component. this works for any s3 compatible storage:

if you do patch it yourself, the initializer should look like this
import { apiInitializer } from "discourse/lib/api";

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

  // ⚠️ Ensure these are your exact domains!
  // look at the image paths for post thumbnail & chat thumbnail and compare!

  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. Safely fix images rendered in standard posts/HTML
  api.decorateCooked($elem => {
    const container = $elem[0];
    if (!container) return;
    
    container.querySelectorAll(`img[src*="${badDomain}"]`).forEach(fixImage);
  }, { id: 'fix-r2-chat-bug' });

  // 2. High-performance observer strictly for dynamically injected Chat elements
  const observer = new MutationObserver(mutations => {
    for (const mutation of mutations) {
      
      // If a new chat message or element is injected into the DOM
      if (mutation.type === 'childList') {
        for (const node of mutation.addedNodes) {
          if (node.nodeType === 1) { // ELEMENT_NODE
            if (node.tagName === 'IMG') fixImage(node);
            
            // Only search INSIDE the new node, not the whole document
            node.querySelectorAll(`img[src*="${badDomain}"]`).forEach(fixImage);
          }
        }
      } 
      // If an existing image's src attribute changes (lazy loading)
      else if (mutation.type === 'attributes') {
        if (mutation.target.tagName === 'IMG') {
          fixImage(mutation.target);
        }
      }
    }
  });

  // Start the observer immediately
  observer.observe(document.body, {
    childList: true,
    subtree: true,
    attributes: true,
    attributeFilter: ['src', 'data-src']
  });
});
3 Likes

It seems that this isn’t a problem that only happens with the chat. I just moved a forum to R2. The new images uploaded all have the correct path, the S3 CDN, but the old ones have the endpoint address. And for me, they only show up if I install the TC from @Lilly.

Here’s some samples:


1 Like

oh for old images (not a brand new forum) you need to run the rake task:

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

then rebake the local uploads:

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

thanks for reminding me, i will add this to my R2 object storage documentation

2 Likes

Yes, i have already tried that, it’s still the endpoint URL, like this:
(https://bucketname.xxxx.r2.cloudflarestorage.com/original/1X/ac9e68ba07090c0d2d8fd3e38338e75e1e328448.jpeg)

1 Like

did you add this line to your app.yml (or add the domain to the S3 CDN URL admin setting)?

DISCOURSE_S3_CDN_URL: https://your.R2.domain.com # (your actual R2 custom domain)

everything has to be in the correct order of operations too.

  1. admin/app.yml settings
  2. rake migrate
  3. rebake posts

there is also the string replacement tool that may help - run inside containter (swap out the strings for your specific ones):

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

Yes, all looks right and the remap does not fix it. The old thimbnails are still broken, but the image appears if i click on it or if i enable the tc.

1 Like

sorry that didn’t work for you.

i have a PR to fix the bug here:

2 Likes