Skip to content

Commit bc559ca

Browse files
iHiDclaude
andauthored
Serve ActiveStorage attachments via the proxy endpoint (#9422)
Partner/advert logos are rendered with `image_tag advert.light_logo`, which resolves to ActiveStorage's blob *redirect* endpoint. That endpoint 302s to a presigned S3 URL, and Rails serves the redirect as `Cache-Control: max-age=..., private` (`expires_in ActiveStorage.service_urls_expire_in`, no `public: true`), capped at the signature's lifetime. Cloudflare never caches a `private` response, so the two advert logos were hitting origin ~7k times each per 12h. Switching `resolve_model_to_route` to `:rails_storage_proxy` gives a permanent URL served by `ActiveStorage::Blobs::ProxyController`, which sets `public, max-age=<forever>` and disables the session, so there's no `Set-Cookie` to bypass the edge either. Cloudflare fetches each blob once per PoP. Claude-Session: https://claude.ai/code/session_01JLa7RVZtKEECSsYczu458W Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent a419531 commit bc559ca

2 files changed

Lines changed: 45 additions & 4 deletions

File tree

config/application.rb

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,18 @@ class Application < Rails::Application
3333
# Allow SVGs to render from active storage
3434
config.active_storage.content_types_to_serve_as_binary -= ['image/svg+xml']
3535

36-
# Public assets (partner logos, avatars) are served via ActiveStorage's
37-
# blob redirect endpoint, which is hit on every page load. Extending the
38-
# signed URL expiry lets browsers/CDNs cache the redirect for longer,
39-
# cutting repeat hits to that endpoint.
36+
# Public assets (partner logos, avatars) are hit on every page load, so
37+
# they need to be cacheable at the edge. The default blob *redirect*
38+
# endpoint can never be: it 302s to a presigned S3 URL, so Rails marks the
39+
# redirect `private` and caps its max-age at the signature's lifetime.
40+
#
41+
# Proxying instead means the URL is permanent and the response is served
42+
# with `public, max-age=<forever>` (ActiveStorage::Blobs::ProxyController),
43+
# so Cloudflare fetches each blob once and never asks again.
44+
config.active_storage.resolve_model_to_route = :rails_storage_proxy
45+
46+
# Still relevant for anything that asks a blob for its service URL directly
47+
# (direct downloads, the admin UI) rather than going through a route.
4048
config.active_storage.service_urls_expire_in = 1.day
4149

4250
Rails.autoloaders.main.ignore(Rails.root.join('app', 'css'))
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
require 'test_helper'
2+
3+
class ActiveStorageCachingTest < ActionDispatch::IntegrationTest
4+
test "attachments route to the proxy endpoint, not the redirect endpoint" do
5+
partner = create :partner
6+
attach_logo(partner)
7+
8+
url = Rails.application.routes.url_helpers.url_for(partner.light_logo)
9+
10+
assert_includes url, "/rails/active_storage/blobs/proxy/"
11+
end
12+
13+
test "proxied attachments are publicly cacheable at the edge" do
14+
partner = create :partner
15+
attach_logo(partner)
16+
17+
get Rails.application.routes.url_helpers.url_for(partner.light_logo)
18+
19+
assert_response :success
20+
assert_includes response.headers["Cache-Control"], "public"
21+
refute_includes response.headers["Cache-Control"].to_s, "private"
22+
assert_nil response.headers["Set-Cookie"]
23+
end
24+
25+
private
26+
def attach_logo(partner)
27+
partner.light_logo.attach(
28+
io: File.open(Rails.root.join("test", "fixtures", "test.jpg")),
29+
filename: "test.jpg",
30+
content_type: "image/jpeg"
31+
)
32+
end
33+
end

0 commit comments

Comments
 (0)