# Custom Emojis loaded from S3/R2 bypass CDN routing

**URL:** https://meta.discourse.org/t/custom-emojis-loaded-from-s3-r2-bypass-cdn-routing/406384
**Category:** Bug
**Tags:** emoji, cdn, s3, fixed
**Created:** [June 29, 2026, 7:50pm UTC](https://meta.discourse.org/t/custom-emojis-loaded-from-s3-r2-bypass-cdn-routing/406384 "2026-06-29T19:50:56Z")
**Posts on this page:** 13
**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: [June 29, 2026, 7:50pm UTC](https://meta.discourse.org/t/custom-emojis-loaded-from-s3-r2-bypass-cdn-routing/406384/1 "2026-06-29T19:50:56Z")

</div>

### Overview

when using S3 or cloudflare R2 for uploads alongside a custom CDN URL, custom emoji uploads do not respect the CDN configuration and attempt to load directly from the raw bucket URL.

### The issue

when an admin uploads a custom emoji, the uploader creates an `upload` record and saves the raw bucket URL to the database (e.g., `//my-bucket.s3.amazonaws.com/...` or `//my-bucket.r2.cloudflarestorage.com/...`) - this is standard Discourse behavior.

however, when `app/models/emoji.rb` generates the emoji cache for `/site.json`, it passes the `upload.url` directly to the `emoji` object:

```plaintext
e.url = emoji.upload&.url

```

because it skips the CDN helper, the frontend receives the raw bucket URL. so depending on how strict the bucket’s access policies are, this results in broken images or forces Discourse to route the emojis through the internal `avatar_proxy`.

### Solution

i have opened a PR that wraps the URL assignments in `Discourse.store.cdn_url()`, which makes the custom emoji loader align with how standard post images and avatars are routed.

> <https://github.com/discourse/discourse/pull/41267>
>
> When a Discourse instance is configured to use S3 (or S3-compatible storage like… Cloudflare R2) for uploads, the Custom Emoji uploader saves the raw bucket URL to the \`uploads\` table (e.g., \`//my-bucket.s3.amazonaws.com/...\`).
> 
> Currently, when the Emoji cache is generated for \`/site.json\`, \`app/models/emoji.rb\` passes \`emoji.upload&.url\` directly. If a dedicated App CDN (\`DISCOURSE\_CDN\_URL\`) is not configured, this causes newly uploaded Custom Emojis to completely bypass the configured \`DISCOURSE\_S3\_CDN\_URL\` and attempt to load directly from the raw bucket URL, resulting in broken images. 
> 
> Additionally, during live uploads via the Admin UI, the API response returns a broken relative path, resulting in a broken image preview until the page is forcibly refreshed.
> 
> \*\*Why \`DISCOURSE\_CDN\_URL\` is intentionally omitted in this architecture:\*\*
> While the standard advice is to configure an App CDN, this is physically impossible for admins who proxy their main domain through Cloudflare to handle their app caching. 
> If an admin attempts to define a secondary \`DISCOURSE\_CDN\_URL\` (e.g., \`cdn.forum.com\`) using a standard Cloudflare DNS record, Cloudflare cannot rewrite the \`Host\` header to match the origin domain (to prevent Domain Fronting). Because the \`Host\` header doesn't match \`DISCOURSE\_HOSTNAME\`, Discourse's internal NGINX server rejects the CDN requests with a \`301 Redirect\`, instantly triggering CORS errors and breaking the site's Javascript. 
> 
> Therefore, admins using the Cloudflare Proxy + S3/R2 architecture \*must\* leave \`DISCOURSE\_CDN\_URL\` blank while setting \`DISCOURSE\_S3\_CDN\_URL\`.
> 
> \*\*The Solution:\*\*
> This PR wraps the assignment in \`Discourse.store.cdn\_url()\` so that Custom Emoji uploads obey the same S3 CDN routing and fallback logic as standard post image uploads. This ensures the emojis route to the S3 CDN successfully, and fixes the broken API response generation during live uploads at the backend level.
> 
> \*\*Files touched:\*\*
> \* \`app/models/emoji.rb\`
> \* \`spec/models/emoji\_spec.rb\`

### Interim fix

while waiting for the PR to be reviewed and merged, i created a lightweight theme component that swaps the raw bucket URL for the proper CDN URL right before the custom emoji renders in the DOM (works for both posts and chat).

if you are experiencing this bug on your site, you can install this component and configure your S3 strings in the theme admin settings to fix any broken custom emojis:

> **[GitHub - Lillinator/s3-fix-custom-emojis: provides a fix for the s3 object storage bug...](https://github.com/Lillinator/s3-fix-custom-emojis)**
>
> provides a fix for the s3 object storage bug related to custom emoji rendering

**Note:** if you have already uploaded custom emojis that are currently borked, running `discourse remap "//my-raw-bucket-url.com" "https://my-cdn.com"` in the container will fix the old ones in the database, while the theme component will fix any newly uploaded ones until the PR fix is merged into core.

---

<div class="post-metadata">

### Author: ![Falco](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/falco/32/179432_2.png) [@Falco](https://meta.discourse.org/u/Falco)
#### Post date: [June 29, 2026, 7:59pm UTC](https://meta.discourse.org/t/custom-emojis-loaded-from-s3-r2-bypass-cdn-routing/406384/2 "2026-06-29T19:59:52Z")

</div>

Testing a default set emoji

😃

Testing a custom emoji

:falco:

---

<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: [June 29, 2026, 8:00pm UTC](https://meta.discourse.org/t/custom-emojis-loaded-from-s3-r2-bypass-cdn-routing/406384/3 "2026-06-29T20:00:35Z")

</div>

yea maybe it’s a cloudflare R2 only thing then. they are breaking on my instance. it only seems to break for new ones uploaded too.

without the theme component fix, i have to run the remap every time i upload a new one. the PR might need a bit of work - i’m not super good with the emoji code.

---

<div class="post-metadata">

### Author: ![Falco](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/falco/32/179432_2.png) [@Falco](https://meta.discourse.org/u/Falco)
#### Post date: [June 29, 2026, 8:04pm UTC](https://meta.discourse.org/t/custom-emojis-loaded-from-s3-r2-bypass-cdn-routing/406384/4 "2026-06-29T20:04:17Z")

</div>

Discourse uses a two CDN setup, one for assets and one proxying the app.

Standard emoji uses one CDN, custom emoji uses the other CDN, but both are protected by a CDN on a properly configured website with a working two CDN setup.

I went over that in the first related topic here

> [@Custom emoji don't use CDN for S3 stored assets in a few pages](https://meta.discourse.org/t/custom-emoji-dont-use-cdn-for-s3-stored-assets-in-a-few-pages/159787/2):
>
> Custom emoji for testing facepalmmdr Non custom emoji slightly_smiling_face So standard emoji use one CDN while custom use another, but both are covered by CDN if you do a proper configuration following [Using Object Storage for Uploads (S3 Clones)](https://meta.discourse.org/t/using-object-storage-for-uploads-s3-clones/148916) to the letter. Not a bug, this is expected behavior because standard emoji is stored inside the app codebase and custom is just a normal site upload.

Does your site have both CDNs setup and working?

---

<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: [June 29, 2026, 8:04pm UTC](https://meta.discourse.org/t/custom-emojis-loaded-from-s3-r2-bypass-cdn-routing/406384/5 "2026-06-29T20:04:49Z")

</div>

oh i’ll have to have a look - i didn’t know that. thanks!

edit:

> [@Custom emoji don't use CDN for S3 stored assets in a few pages](https://meta.discourse.org/t/custom-emoji-dont-use-cdn-for-s3-stored-assets-in-a-few-pages/159787/2):
>
> So standard emoji use one CDN while custom use another, but both are covered by CDN if you do a proper configuration following [Using Object Storage for Uploads (S3 Clones)](https://meta.discourse.org/t/using-object-storage-for-uploads-s3-clones/148916) to the letter.

i wrote the section for Cloudflare R2 so assume i have it setup correctly? what might i be missing?

---

<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: [June 29, 2026, 8:52pm UTC](https://meta.discourse.org/t/custom-emojis-loaded-from-s3-r2-bypass-cdn-routing/406384/6 "2026-06-29T20:52:18Z")

</div>

![IMG_0127](https://global.discourse-cdn.com/meta/original/4X/e/9/b/e9ba8319d252a55bbe90af33bcbd1487a8ccc5b5.jpeg)

---

<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: [June 29, 2026, 11:35pm UTC](https://meta.discourse.org/t/custom-emojis-loaded-from-s3-r2-bypass-cdn-routing/406384/7 "2026-06-29T23:35:48Z")

</div>

just confirming here that i installed and tested the PR branch after the recent changes to it and it fixes the issue.

---

<div class="post-metadata">

### Author: ![Falco](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/falco/32/179432_2.png) [@Falco](https://meta.discourse.org/u/Falco)
#### Post date: [June 30, 2026, 5:28pm UTC](https://meta.discourse.org/t/custom-emojis-loaded-from-s3-r2-bypass-cdn-routing/406384/8 "2026-06-30T17:28:01Z")

</div>

> [@Lilly](#):
>
> i wrote the section for Cloudflare R2 so assume i have it setup correctly? what might i be missing?

It is this section of the wiki guide I wrote:

> [@Configure an S3 compatible object storage provider for uploads](https://meta.discourse.org/t/configure-an-s3-compatible-object-storage-provider-for-uploads/148916/1):
>
> DISCOURSE\_CDN\_URL is a CDN that points to you Discourse hostname and caches requests. It will be used mainly for pullable assets: CSS and other theme assets.
> 
> DISCOURSE\_S3\_CDN\_URL is a CDN that points to your object storage bucket and caches requests. It will be mainly used for pushable assets: JS, images and user uploads.
> 
> We recommend those being different and for admins to set both.

Do you have both set ?

> [@Lilly](#):
>
> ![IMG_0127](https://global.discourse-cdn.com/meta/original/4X/e/9/b/e9ba8319d252a55bbe90af33bcbd1487a8ccc5b5.jpeg)

This is an artifact of moving Meta from AWS back to Metal, it was missing a new cook, just rebuilt HTML to fix it.

> [@Lilly](#):
>
> just confirming here that i installed and tested the PR branch after the recent changes to it and it fixes the issue.

Do your test site have both CDN ENV vars set?

---

<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: [June 30, 2026, 6:48pm UTC](https://meta.discourse.org/t/custom-emojis-loaded-from-s3-r2-bypass-cdn-routing/406384/9 "2026-06-30T18:48:47Z")

</div>

> [@Falco](#):
>
> Do your test site have both CDN ENV vars set?

i do have them set, but for gods sake i had a typo in my cloudflare config for the cdn-specific dns record that `DISCOURSE_CDN_URL` points to and i never tested it when i set it up because my site was working 😆 what a gong show.

at least i learned more about emoji code creating that pointless PR…  
🤦🏻‍♀️

thanks Falco!

---

<div class="post-metadata">

### Author: ![Falco](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/falco/32/179432_2.png) [@Falco](https://meta.discourse.org/u/Falco)
#### Post date: [June 30, 2026, 7:48pm UTC](https://meta.discourse.org/t/custom-emojis-loaded-from-s3-r2-bypass-cdn-routing/406384/10 "2026-06-30T19:48:59Z")

</div>

Ha, no worries!

> [@Lilly](#):
>
> at least i learned more about emoji code creating that pointless PR…

That is how I learn the most, chasing rabbit holes that will never amount to anything tangible. But the knowledge from those is golden!

---

<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: [July 1, 2026, 12:26am UTC](https://meta.discourse.org/t/custom-emojis-loaded-from-s3-r2-bypass-cdn-routing/406384/11 "2026-07-01T00:26:16Z")

</div>

wow what a ride this was. i basically reconfigured my whole Cloudfare R2 object storage and Discourse instance, and i think this bug is legit for R2. when i fixed my cloudflare dns record and rebuilt the instance so that `DISCOURSE_CDN_URL` actually pointed to it, it borked a bunch of other stuff like translation strings and threw multiple errors in the console including some CORS errors. it led me down many rabbit holes today. so i guess using `DISCOURSE_CDN_URL` seems incompatible with Cloudflare R2. (this was very weird - when i originally setup my original dns entry, i had incorrectly entered the `cdn.mysite.com` dns record so that it was resolving as `cdn.mysite.com.mysite.com`). setting `DISCOURSE_CDN_URL` correctly seems incompatible with Cloudflare R2 object storage. there may be some other stuff i’m not fully understanding here.

anyways, when i rebuild with my PR branch it is all fixed because it wraps the assignment in `Discourse.store.cdn_url()` so that custom emojis uploads obey the same S3 CDN routing and fallback logic as standard post image uploads.

i reopened the PR and edited the description. but i guess if Discourse team chooses not to merged it, that is fine because the theme component fixes the issue at the client level. Note the PR fix should only affect R2 object storage configurations for custom emoji and not other regular S3 compatible like AWS.

---

<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: [July 2, 2026, 2:29am UTC](https://meta.discourse.org/t/custom-emojis-loaded-from-s3-r2-bypass-cdn-routing/406384/12 "2026-07-02T02:29:48Z")

</div>

> [@Lilly](#):
>
> i have opened a PR that wraps the URL assignments in `Discourse.store.cdn_url()`, which makes the custom emoji loader align with how standard post images and avatars are routed.
> 
> [FIX: Apply CDN to Custom Emojis loaded from S3/R2 - Pull Request #41267 - discourse/discourse - GitHub](https://github.com/discourse/discourse/pull/41267)

this PR was merged and the custom emoji are now fixed when using Cloudflare R2 object storage for uploads. I have posted a suggested update to the relevant R2 documentation here: [Configure an S3 compatible object storage provider for uploads](https://meta.discourse.org/t/configure-an-s3-compatible-object-storage-provider-for-uploads/148916)

---

<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: [July 3, 2026, 7:35pm UTC](https://meta.discourse.org/t/custom-emojis-loaded-from-s3-r2-bypass-cdn-routing/406384/15 "2026-07-03T19:35:12Z")

</div>

Issue is resolved, file a new report if you see this again
