Skip to content

Fix rich text image display with S3-compatible storage#2835

Open
dilberryhoundog wants to merge 1 commit intobasecamp:mainfrom
dilberryhoundog:fix/rich-text-image-cors-and-urls
Open

Fix rich text image display with S3-compatible storage#2835
dilberryhoundog wants to merge 1 commit intobasecamp:mainfrom
dilberryhoundog:fix/rich-text-image-cors-and-urls

Conversation

@dilberryhoundog
Copy link
Copy Markdown

Summary

  • Fix service worker CORS credentials to work with cross-origin Active Storage redirects to S3/R2/GCS/Azure
  • Fix ActionText attachment URLs missing account prefix for multi-tenant routing

Problem

Two issues affecting self-hosted deployments using S3-compatible storage (R2, MinIO, etc.):

  1. Service worker CORS failure: The turbo-offline service worker inherits credentials: "include" from the original request when fetching Active Storage redirect URLs. After following the cross-origin redirect to cloud storage, the browser requires Access-Control-Allow-Credentials: true in the response, which S3-compatible stores cannot return with specific origin CORS policies.

  2. ActionText attachment URLs missing account prefix: to_rich_text_attributes generates blob URLs without the account script_name prefix (e.g., /rails/active_storage/blobs/redirect/... instead of /1234567/rails/active_storage/blobs/redirect/...). The AccountSlug::Extractor middleware can't route these, returning 404. The editor (Lexxy) uses these URLs as <img src> when re-editing rich text, so images appear broken in the editor despite displaying correctly on the show view.

Root cause: upstream commit 235890e66 switched ActionText attachment URLs from absolute to relative for portability, but the relative URLs lack the script_name prefix needed by multi-tenant routing.

Changes

app/views/pwa/service_worker.js.erb (line 46):

-    fetchOptions: { mode: "cors" }
+    fetchOptions: { mode: "cors", credentials: "same-origin" }

Sends credentials to Rails (needed for auth) but strips them on cross-origin redirects to cloud storage.

config/initializers/active_storage.rb (line 17):

-      super.merge url: Rails.application.routes.url_helpers.polymorphic_url(self, only_path: true)
+      super.merge url: Rails.application.routes.url_helpers.polymorphic_url(self, only_path: true, script_name: Current.account&.slug)

Adds account slug prefix so URLs route correctly through the multi-tenant middleware. Nil-safe — when Current.account is nil, behaves identically to before.

Safety

  • Disk service: Same-origin redirects still receive credentials; account-prefixed URLs route correctly through middleware
  • S3/R2/GCS/Azure: Cross-origin credentials stripped (desired); blob URLs use service's own URL generation (unaffected by script_name)
  • Background jobs: AccountTenanted mixin serializes/restores Current.account
  • Non-request contexts: Current.account&.slug returns nil, no behavior change
  • Existing pattern: Webhook delivery already uses script_name: account.slug with polymorphic_url

Reproduction

  • Storage: Cloudflare R2 with private blobs served via signed URLs
  • Tenant config: Single-tenant deployment (MULTI_TENANT=false), but still uses path-based account slug routing
  • Active Storage mode: Redirect mode (default, blobs served via 302 to storage provider)
  • Editor: Latest Lexxy gallery implementation (0.9.5) triggering the service worker to cache image representations

Note

Existing ActionText content with broken URLs will fix itself when re-saved.

Fix service worker CORS credentials to work with cross-origin Active Storage redirects to S3/R2/GCS/Azure. The turbo-offline service worker inherits credentials: include from the original request, which fails after the cross-origin redirect because S3-compatible stores cannot return Access-Control-Allow-Credentials: true. Using credentials: same-origin sends credentials to Rails for auth but strips them on the cross-origin redirect.

Fix ActionText attachment URLs missing account prefix for multi-tenant routing. to_rich_text_attributes generates blob URLs without the account script_name prefix, so AccountSlug::Extractor cannot route them. Adding script_name: Current.account&.slug produces correct tenant-prefixed paths. Nil-safe when no account is set.
Copilot AI review requested due to automatic review settings April 11, 2026 12:24
Copy link
Copy Markdown

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Fixes rich-text image rendering and offline caching in self-hosted deployments that use cross-origin Active Storage redirects (S3-compatible providers), while preserving multi-tenant path-based routing.

Changes:

  • Forces the PWA service worker to use credentials: "same-origin" so cross-origin redirects to storage don’t require Access-Control-Allow-Credentials.
  • Adds an account slug script_name prefix to ActionText attachment URLs so multi-tenant routing can resolve blob/representation redirect paths.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
config/initializers/active_storage.rb Prefixes ActionText attachment relative URLs with the tenant/account path segment via script_name.
app/views/pwa/service_worker.js.erb Updates turbo-offline fetch options to avoid sending credentials on cross-origin redirects to blob storage.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

# path (for portability across host name changes, beta environments, etc).
def to_rich_text_attributes(*)
super.merge url: Rails.application.routes.url_helpers.polymorphic_url(self, only_path: true)
super.merge url: Rails.application.routes.url_helpers.polymorphic_url(self, only_path: true, script_name: Current.account&.slug)
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

script_name is expected to be a path prefix (typically starting with /). Passing a bare slug (e.g., \"123\") can produce malformed paths (missing separator) depending on how Rails concatenates script_name + path. Consider normalizing to script_name: \"/#{Current.account.slug}\" (or using a helper that already returns a leading-slash prefix) so generated URLs are reliably /123/....

Suggested change
super.merge url: Rails.application.routes.url_helpers.polymorphic_url(self, only_path: true, script_name: Current.account&.slug)
slug = Current.account&.slug
super.merge url: Rails.application.routes.url_helpers.polymorphic_url(
self,
only_path: true,
script_name: (slug.present? ? "/#{slug.delete_prefix("/")}" : nil)
)

Copilot uses AI. Check for mistakes.
maxEntrySize: 2 * 1024 * 1024, // 2MB covers about 95% of all Fizzy blobs
maxEntries: 500,
fetchOptions: { mode: "cors" }
fetchOptions: { mode: "cors", credentials: "same-origin" }
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is subtle but security/CORS-sensitive (it intentionally strips credentials on cross-origin redirects). Adding an inline comment explaining the redirect/CORS rationale here would help prevent future regressions (e.g., someone reverting to include while debugging auth).

Suggested change
fetchOptions: { mode: "cors", credentials: "same-origin" }
fetchOptions: {
mode: "cors",
// Intentionally avoid `include`: with CORS requests, `same-origin` strips
// credentials on cross-origin redirects, which is the safer behavior here.
credentials: "same-origin"
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants