Cloudflare R2 이미지 URL 표시 문제: 상세 설명 및 해결 방법

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개의 좋아요

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

Thank you for your very helpful report here!

1개의 좋아요

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개의 좋아요

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

도메인 설정이 포함된 업데이트된 테마 컴포넌트로 이 문제를 해결했습니다. 이제 누구나 자체 컴포넌트를 하드코딩하지 않고도 이를 사용할 수 있습니다. 이는 모든 S3 호환 저장소에서 작동합니다:

직접 패치하는 경우, 이니셜라이저는 다음과 같이 작성해야 합니다
import { apiInitializer } from "discourse/lib/api";

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

  // ⚠️ 이 도메인이 정확히 여러분의 도메인인지 확인하세요!
  // 게시글 썸네일 및 채팅 썸네일의 이미지 경로를 확인하고 비교해 보세요!

  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. 표준 게시글/HTML에서 렌더링된 이미지를 안전하게 수정
  api.decorateCooked($elem => {
    const container = $elem[0];
    if (!container) return;
    
    container.querySelectorAll(`img[src*="${badDomain}"]`).forEach(fixImage);
  }, { id: 'fix-r2-chat-bug' });

  // 2. 동적으로 주입된 채팅 요소에만 적용되는 고성능 옵저버
  const observer = new MutationObserver(mutations => {
    for (const mutation of mutations) {
      
      // 새 채팅 메시지나 요소가 DOM에 주입된 경우
      if (mutation.type === 'childList') {
        for (const node of mutation.addedNodes) {
          if (node.nodeType === 1) { // ELEMENT_NODE
            if (node.tagName === 'IMG') fixImage(node);
            
            // 새 노드 내부에서만 검색하며, 문서 전체를 검색하지 않음
            node.querySelectorAll(`img[src*="${badDomain}"]`).forEach(fixImage);
          }
        }
      } 
      // 기존 이미지의 src 속성이 변경된 경우 (레이지 로딩)
      else if (mutation.type === 'attributes') {
        if (mutation.target.tagName === 'IMG') {
          fixImage(mutation.target);
        }
      }
    }
  });

  // 옵저버를 즉시 시작
  observer.observe(document.body, {
    childList: true,
    subtree: true,
    attributes: true,
    attributeFilter: ['src', 'data-src']
  });
});
3개의 좋아요

이 문제는 채팅에서만 발생하는 것이 아닌 것 같습니다. 방금 포럼을 R2로 이전했는데, 새로 업로드된 이미지들은 모두 S3 CDN이라는 올바른 경로를 가지고 있지만, 기존 이미지들은 엔드포인트 주소를 사용하고 있습니다. 그리고 제 경우엔 @Lilly님의 TC를 설치해야만 표시가 됩니다.

샘플을 몇 가지 첨부합니다:


1개의 좋아요

오래된 이미지의 경우(아주 새로운 포럼이 아닌 경우) 다음 rake 작업을 실행해야 합니다:

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

그 다음 로컬 업로드를 다시 베이킹합니다:

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

기억해 줘서 감사합니다. 이 내용을 제 R2 객체 저장소 문서에 추가하겠습니다.

2개의 좋아요

네, 이미 그 방법을 시도해 보았지만, 여전히 엔드포인트 URL이 다음과 같은 형태로 표시됩니다:
(https://bucketname.xxxx.r2.cloudflarestorage.com/original/1X/ac9e68ba07090c0d2d8fd3e38338e75e1e328448.jpeg)

1개의 좋아요

이 줄을 app.yml에 추가했거나(S3 CDN URL 관리자 설정에 도메인을 추가했거나) 하셨나요?

DISCOURSE_S3_CDN_URL: https://your.R2.domain.com # (실제 R2 커스텀 도메인)

모든 작업이 올바른 순서로 수행되어야 합니다.

  1. 관리자/app.yml 설정
  2. rake migrate
  3. 포스트 재배기 (rebake)

도움이 될 수 있는 문자열 치환 도구도 있습니다 - 컨테이너 내에서 실행하세요 (문자열을 특정 값으로 교체):

discourse remap "https://<cloudflare-account-id>.r2.cloudflarestorage.com/<R2-bucket-name>" "https://your.cdn.domain.com"
2개의 좋아요

네, 모든 것이 정상적으로 보이고 리맵으로도 해결되지 않습니다. 이전 썸네일 여전히 깨져 있지만, 이미지를 클릭하거나 TC를 활성화하면 이미지가 표시됩니다.

1개의 좋아요

안 돼서 죄송합니다.

버그를 수정하는 PR을 여기에 올렸습니다:

2개의 좋아요