# 聊天缩略图绕过 s3\_cdn\_url 并使用原始 S3 存储桶 URL

**URL:** https://meta.discourse.org/t/chat-thumbnails-bypass-s3-cdn-url-use-raw-s3-bucket-urls/404174
**Category:** Bug
**Tags:** cdn, s3, chat, fixed
**Created:** [2026年五月31日 22:13 UTC](https://meta.discourse.org/t/chat-thumbnails-bypass-s3-cdn-url-use-raw-s3-bucket-urls/404174 "2026-05-31T22:13:47Z")
**Posts on this page:** 10
**Page:** 1

<div class="post-metadata">

### Author: ![Lilly](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/lilly/32/575047_2.png) [@Lilly](https://meta.discourse.org/u/Lilly)
#### Post date: [2026年五月31日 22:13 UTC](https://meta.discourse.org/t/chat-thumbnails-bypass-s3-cdn-url-use-raw-s3-bucket-urls/404174/1 "2026-05-31T22:13:47Z")

</div>

我最近配置了 Cloudflare R2 上传存储桶，结果聊天缩略图显示异常。于是我深入排查并快速修复了配置，随后发现了这个主题：[https://meta.discourse.org/t/cloudflare-r2-image-url-display-issue-detailed-explanation-and-fix/358204。无论如何，我查看了其他](https://meta.discourse.org/t/cloudflare-r2-image-url-display-issue-detailed-explanation-and-fix/358204%E3%80%82%E6%97%A0%E8%AE%BA%E5%A6%82%E4%BD%95%EF%BC%8C%E6%88%91%E6%9F%A5%E7%9C%8B%E4%BA%86%E5%85%B6%E4%BB%96) S3 上传存储桶的配置，发现这个问题并非 Cloudflare 特有。

* * *

## 描述

当为上传配置了外部 S3 或兼容的对象存储时，聊天缩略图会绕过 CDN，直接从存储桶 URL 加载。

对于像 Cloudflare R2 这样需要安全配置的外部 S3 兼容存储桶，聊天缩略图会损坏且无法显示。

根本问题在于聊天序列化器未能将 `s3_cdn_url` 设置应用于缩略图。图像未通过配置的 CDN 进行路由，而是将原始的 S3 存储桶内部 URL 直接泄露到了浏览器负载中。

## 复现步骤

在 Meta 及其他使用 S3 上传存储桶的站点上均可复现此问题：

1. 在聊天或频道中发布一张图片
2. 在控制台检查缩略图图片的 URL
3. 点击图片以获取更大的原图，并检查其 URL
4. 将其与缩略图 URL 进行对比

以下是来自 Meta 聊天的示例：

**缩略图 URL** ：来自存储桶

```plaintext
https://cdck-file-uploads-global.s3.dualstack.us-west-2.amazonaws.com/meta/optimized/4X/4/7/9/479815360e0e6e0cd9f4ba565891776e84aea532_2_375x500.jpeg

```

**原图 URL** ：通过 CDN

```plaintext
https://global.discourse-cdn.com/meta/original/4X/4/7/9/479815360e0e6e0cd9f4ba565891776e84aea532.jpeg

```

在控制台中，缩略图的 HTML 代码 `<img...` 包含 `data-large-src`（CloudFront CDN URL）和 `src`（AWS 存储桶 URL）。

> **截图**
>
> ![image](https://global.discourse-cdn.com/meta/original/4X/8/2/7/8272d1de89d8351be54aceaf3255bdb21ad5f6f3.png)

## 影响：

- 对于像 Cloudflare R2 这样默认安全且阻止未授权访问原始存储桶端点的 S3 兼容存储，聊天缩略图（优化后的）会损坏。
- 对于允许访问原始存储桶端点的 AWS 及其他 S3 兼容对象存储桶，会导致带宽泄露，因为聊天完全绕过了 CDN；这将导致为所有聊天缩略图流量支付直接的 S3 出站费用。
- 基础设施泄露：原始后端存储 URL（包括内部存储桶名称，有时甚至包括账户 ID）正被暴露在客户端 JSON 负载中。

### 拉取请求（PR）：

我提交了一个修复该问题的 PR：

> <https://github.com/discourse/discourse/pull/40419>
>
> \### What does this PR do?
> This PR fixes a bug where Discourse Chat bypasses the… configured \`s3\_cdn\_url\` and exposes raw S3 bucket URLs in the chat stream. 
> 
> It addresses two separate issues preventing the CDN from applying properly:
> 1. \*\*Missing Wrappers in Chat:\*\* The \`chat-upload.gjs\` component was piping raw database URLs directly into the \`\<img src\>\`, \`\<video\>\`, and \`\<audio\>\` tags. This PR imports and applies the \`getURLWithCDN\` utility to all upload types in the chat stream.
> 2. \*\*Protocol Mismatches in \`getURLWithCDN\`:\*\* The JavaScript utility relied on a strict \`.startsWith()\` comparison between the \`url\` and \`S3BaseUrl\`. When the server configures \`S3BaseUrl\` with \`https://\` but sends the payload \`url\` as protocol-relative (\`//\`), the match fails and the CDN is ignored. This PR normalizes both strings by stripping the protocol before comparison, mirroring how Ruby handles domain matching on the backend.
> 
> \### Why is it needed?
> Bypassing the CDN for chat uploads causes two major issues:
> \* \*\*Broken UI for Secure Buckets:\*\* For providers like Cloudflare R2 that block direct, unauthenticated bucket access, bypassing the CDN results in broken images (\`403 Forbidden\`). 
> \* \*\*Bandwidth Leaks:\*\* For standard setups (AWS, DigitalOcean), bypassing the CDN means admins are paying raw S3 egress fees for chat thumbnail traffic instead of utilizing their configured CDN. \*(Note: This bug is currently visible on Meta, where chat thumbnails are serving directly from \`assets-meta-cdck-prod-meta.s3.dualstack.us-west-1.amazonaws.com\` instead of CloudFront).\*
> 
> \### How was this tested?
> \* Added unit tests to \`get-url-test.js\` to verify \`getURLWithCDN\` successfully applies the CDN even when protocol strings mismatch (e.g., \`https://\` vs \`//\`).
> \* Deployed to a cloud dev instance using Cloudflare R2. Verified that both original uploads and optimized thumbnails in the Chat stream now successfully route through the configured \`s3\_cdn\_url\` domain without breaking existing attachment functionality.
> 
> \*\*Files Touched:\*\*
> \* \`frontend/discourse/app/lib/get-url.js\`
> \* \`frontend/discourse/tests/unit/lib/get-url-test.js\`
> \* \`plugins/chat/assets/javascripts/discourse/components/chat-upload.gjs\`

看起来 [Sam 添加了](https://meta.discourse.org/t/upload-images-in-chat-cant-be-show-normally-when-use-s3-cdn/365540/15?u=lilly) `getURLWithCDN` 到聊天编辑器预览中——不过，我认为它并没有应用到聊天流中？

> <https://github.com/discourse/discourse/pull/34507>

我想知道编辑器的修复是否在某些 S3 配置下也失败了，因为 `getURLWithCDN` 在协议不匹配（`//` 与 `https://`）时会崩溃？无论如何，上述 PR 通过为聊天流添加包装器并使其与协议无关，扩展了 Sam 的工作。

### 临时解决方案：

在我意识到这不仅仅是 Cloudflare 的问题之前，我制作了一个轻量级的主题组件。它在浏览器尝试下载之前，拦截聊天 DOM 中的原始 S3 域名，并将其替换为正确的 CDN 域名。这正确地路由了流量并堵住了带宽泄露的漏洞。我对其进行了适配，使其适用于任何 S3 兼容的对象存储。只需两个设置——「原始 S3 存储桶 URL」和「S3 CDN URL」。

> **[GitHub - Lillinator/chat-s3-thumbnails-fix: Fixes the chat thumbnail path bug for Cloudflare...](https://github.com/Lillinator/chat-s3-thumbnails-fix)**
>
> Fixes the chat thumbnail path bug for Cloudflare R2 object storage uploads

~~（不知道为什么 GitHub 的自动链接在这里失效了）~~ 现已修复

---

<div class="post-metadata">

### Author: ![sam](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/sam/32/102149_2.png) [@sam](https://meta.discourse.org/u/sam)
#### Post date: [2026年六月1日 03:09 UTC](https://meta.discourse.org/t/chat-thumbnails-bypass-s3-cdn-url-use-raw-s3-bucket-urls/404174/2 "2026-06-01T03:09:35Z")

</div>

> [@Lilly](#):
>
> （不知道为什么这里的 GitHub 内嵌框坏了）

可能与网站迁移有关……我刚重新烘焙了内容。

---

<div class="post-metadata">

### Author: ![David\_Ghost](https://avatars.discourse-cdn.com/v4/letter/d/c37758/32.png) [@David\_Ghost](https://meta.discourse.org/u/David_Ghost)
#### Post date: [2026年六月11日 15:02 UTC](https://meta.discourse.org/t/chat-thumbnails-bypass-s3-cdn-url-use-raw-s3-bucket-urls/404174/3 "2026-06-11T15:02:58Z")

</div>

你好，PR 已经合并了吗？

谢谢

---

<div class="post-metadata">

### Author: ![Lilly](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/lilly/32/575047_2.png) [@Lilly](https://meta.discourse.org/u/Lilly)
#### Post date: [2026年六月11日 18:31 UTC](https://meta.discourse.org/t/chat-thumbnails-bypass-s3-cdn-url-use-raw-s3-bucket-urls/404174/4 "2026-06-11T18:31:28Z")

</div>

现在它正等待人类团队成员审核，我的好友 discourse-triage-bot 已经修复了一些测试 🙂

[https://github.com/discourse/discourse/pull/40419#issuecomment-4683409099](https://github.com/discourse/discourse/pull/40419#issuecomment-4683409099)

---

<div class="post-metadata">

### Author: ![Lilly](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/lilly/32/575047_2.png) [@Lilly](https://meta.discourse.org/u/Lilly)
#### Post date: [2026年六月15日 18:24 UTC](https://meta.discourse.org/t/chat-thumbnails-bypass-s3-cdn-url-use-raw-s3-bucket-urls/404174/5 "2026-06-15T18:24:46Z")

</div>

看起来已经合并了。

---

<div class="post-metadata">

### Author: ![sam](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/sam/32/102149_2.png) [@sam](https://meta.discourse.org/u/sam)
#### Post date: [2026年六月16日 04:34 UTC](https://meta.discourse.org/t/chat-thumbnails-bypass-s3-cdn-url-use-raw-s3-bucket-urls/404174/6 "2026-06-16T04:34:01Z")

</div>



---

<div class="post-metadata">

### Author: ![darkpixlz](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/darkpixlz/32/549896_2.png) [@darkpixlz](https://meta.discourse.org/u/darkpixlz)
#### Post date: [2026年六月16日 23:00 UTC](https://meta.discourse.org/t/chat-thumbnails-bypass-s3-cdn-url-use-raw-s3-bucket-urls/404174/7 "2026-06-16T23:00:06Z")

</div>

根据作者请求开启

---

<div class="post-metadata">

### Author: ![Lilly](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/lilly/32/575047_2.png) [@Lilly](https://meta.discourse.org/u/Lilly)
#### Post date: [2026年六月16日 23:10 UTC](https://meta.discourse.org/t/chat-thumbnails-bypass-s3-cdn-url-use-raw-s3-bucket-urls/404174/8 "2026-06-16T23:10:59Z")

</div>

对于添加了自定义表情的站点，修复合并后，聊天中的表情现在会失效。

与标准帖子图片不同（可以通过运行 `rake posts:rebake` 修复），聊天自定义表情是通过 `/site.json` 动态传递给前端的。

如果你的数据库中包含缺少协议的 S3 URL（例如 `//bucket.endpoint...`），或者使用了虚拟主机风格域名且该域名与你的 `app.yml` 环境变量不完全匹配，Discourse 的内部 CDN 替换器会静默失败。原始的存储桶 URL 会被传递给浏览器，从而导致聊天中的自定义表情失效。

## 如何修复：

要永久修复此问题，你需要在数据库中强制将原始存储桶 URL 映射到你的 CDN URL，然后清除站点缓存，以便 `/site.json` 重新生成。

### 1. 进入容器：

SSH 登录到你的服务器并进入 Discourse 容器（通常是 `app`，如果你使用双容器设置，则是 `web_only`）。

```bash
cd /var/discourse
./launcher enter app

```

### 2. 重新映射 URL：

运行内置的 Discourse `remap` 工具。你应该运行两次，以捕获 `https://` 变体以及迁移脚本有时会遗留的无协议 `//` 变体。

将占位符替换为你的实际原始存储桶 URL 和实际 CDN URL：

```bash
# 修复标准的 https:// URL
discourse remap "https://<your-bucket>.<your-endpoint>.com" "https://cdn.your-domain.com"

# 修复无协议的 // URL（这通常是导致自定义表情失效的原因）
discourse remap "//<your-bucket>.<your-endpoint>.com" "https://cdn.your-domain.com"

```

### 3. 清除缓存

由于 `/site.json` 被高度缓存，你必须清除 Rails 缓存以强制论坛提供新的 URL：

打开 Rails 控制台：

```bash
rails c

```

运行以下命令：

```ruby
Rails.cache.clear
Site.clear_cache
exit

```

### 4. 刷新

硬刷新你的浏览器（如果仍在使用，请禁用主题组件的临时解决方案）。聊天中的自定义表情现在应该已修复，并通过 CDN 正常加载。

---

<div class="post-metadata">

### Author: ![David\_Ghost](https://avatars.discourse-cdn.com/v4/letter/d/c37758/32.png) [@David\_Ghost](https://meta.discourse.org/u/David_Ghost)
#### Post date: [2026年六月19日 14:33 UTC](https://meta.discourse.org/t/chat-thumbnails-bypass-s3-cdn-url-use-raw-s3-bucket-urls/404174/9 "2026-06-19T14:33:35Z")

</div>

嗨，Lilly，非常感谢你出色的工作。经过两次重新映射后，我之前的上传文件和缩略图现在都能正常显示了。在此之前，就连我的 “/admin/config/customize/themes” 图片都无法加载。现在一切都恢复正常了，真不错。

谢谢！

---

<div class="post-metadata">

### Author: ![system](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/system/32/443519_2.png) [@system](https://meta.discourse.org/u/system)
#### Post date: [2026年六月23日 11:23 UTC](https://meta.discourse.org/t/chat-thumbnails-bypass-s3-cdn-url-use-raw-s3-bucket-urls/404174/10 "2026-06-23T11:23:04Z")

</div>


