Skip to content

Commit 69824b0

Browse files
committed
Turn Propshaft::Server into a proper middleware
Fix: rails/rails#55508 Mounting Propshaft::Server in the Rails router isn't suitable because it end up executed after a long list of middlewares that may set cookies or do other transformation we don't want. In production, propshaft assets are served by `ActionDispatch::Static` (or a reverse proxy), so we should try to be as close to that as possible.
1 parent e49a9de commit 69824b0

File tree

5 files changed

+78
-40
lines changed

5 files changed

+78
-40
lines changed

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,4 +212,4 @@ DEPENDENCIES
212212
rake
213213

214214
BUNDLED WITH
215-
2.5.3
215+
2.7.1

lib/propshaft/assembly.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,11 @@ def resolver
3434
end
3535
end
3636

37-
def server
38-
Propshaft::Server.new(self)
37+
def prefix
38+
@prefix ||= begin
39+
prefix = config.prefix || "/"
40+
prefix.end_with?("/") ? prefix : "#{prefix}/"
41+
end
3942
end
4043

4144
def processor

lib/propshaft/railtie.rb

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,6 @@ class Railtie < ::Rails::Railtie
4242
Pathname.new(File.join(app.config.paths["public"].first, app.config.assets.prefix))
4343
config.assets.manifest_path ||= config.assets.output_path.join(".manifest.json")
4444

45-
app.assets = Propshaft::Assembly.new(app.config.assets)
46-
47-
if config.assets.server
48-
app.routes.prepend do
49-
mount app.assets.server, at: app.assets.config.prefix
50-
end
51-
end
52-
5345
ActiveSupport.on_load(:action_view) do
5446
include Propshaft::Helper
5547
end
@@ -71,6 +63,13 @@ class Railtie < ::Rails::Railtie
7163
end
7264
end
7365

66+
initializer "propshaft.assets_middleware", group: :all do |app|
67+
app.assets = Propshaft::Assembly.new(app.config.assets)
68+
if config.assets.server
69+
app.middleware.insert_after ::ActionDispatch::Static, Propshaft::Server, app.assets
70+
end
71+
end
72+
7473
rake_tasks do
7574
load "propshaft/railties/assets.rake"
7675
end

lib/propshaft/server.rb

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,39 @@
22
require "rack/version"
33

44
class Propshaft::Server
5-
def initialize(assembly)
5+
def initialize(app, assembly)
6+
@app = app
67
@assembly = assembly
78
end
89

910
def call(env)
1011
execute_cache_sweeper_if_updated
11-
path, digest = extract_path_and_digest(env)
12-
13-
if (asset = @assembly.load_path.find(path)) && asset.fresh?(digest)
14-
compiled_content = asset.compiled_content
15-
16-
[
17-
200,
18-
{
19-
Rack::CONTENT_LENGTH => compiled_content.length.to_s,
20-
Rack::CONTENT_TYPE => asset.content_type.to_s,
21-
VARY => "Accept-Encoding",
22-
Rack::ETAG => "\"#{asset.digest}\"",
23-
Rack::CACHE_CONTROL => "public, max-age=31536000, immutable"
24-
},
25-
[ compiled_content ]
26-
]
12+
13+
path = env["PATH_INFO"]
14+
method = env["REQUEST_METHOD"]
15+
16+
if (method == "GET" || method == "HEAD") && path.start_with?(@assembly.prefix)
17+
path, digest = extract_path_and_digest(path)
18+
19+
if (asset = @assembly.load_path.find(path)) && asset.fresh?(digest)
20+
compiled_content = asset.compiled_content
21+
22+
[
23+
200,
24+
{
25+
Rack::CONTENT_LENGTH => compiled_content.length.to_s,
26+
Rack::CONTENT_TYPE => asset.content_type.to_s,
27+
VARY => "Accept-Encoding",
28+
Rack::ETAG => "\"#{asset.digest}\"",
29+
Rack::CACHE_CONTROL => "public, max-age=31536000, immutable"
30+
},
31+
method == "HEAD" ? [] : [ compiled_content ]
32+
]
33+
else
34+
[ 404, { Rack::CONTENT_TYPE => "text/plain", Rack::CONTENT_LENGTH => "9" }, [ "Not found" ] ]
35+
end
2736
else
28-
[ 404, { Rack::CONTENT_TYPE => "text/plain", Rack::CONTENT_LENGTH => "9" }, [ "Not found" ] ]
37+
@app.call(env)
2938
end
3039
end
3140

@@ -34,10 +43,11 @@ def inspect
3443
end
3544

3645
private
37-
def extract_path_and_digest(env)
38-
full_path = Rack::Utils.unescape(env["PATH_INFO"].to_s.sub(/^\//, ""))
46+
def extract_path_and_digest(path)
47+
path = path.delete_prefix(@assembly.prefix)
48+
path = Rack::Utils.unescape(path)
3949

40-
Propshaft::Asset.extract_path_and_digest(full_path)
50+
Propshaft::Asset.extract_path_and_digest(path)
4151
end
4252

4353
if Gem::Version.new(Rack::RELEASE) < Gem::Version.new("3")

test/propshaft/server_test.rb

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,44 +5,70 @@
55
class Propshaft::ServerTest < ActiveSupport::TestCase
66
include Rack::Test::Methods
77

8+
class RackApp
9+
attr_reader :calls
10+
11+
def initialize
12+
@calls = []
13+
end
14+
15+
def call(env)
16+
@calls << env
17+
[200, {}, ["OK"]]
18+
end
19+
end
20+
821
setup do
922
@assembly = Propshaft::Assembly.new(ActiveSupport::OrderedOptions.new.tap { |config|
1023
config.paths = [Pathname.new("#{__dir__}/../fixtures/assets/vendor"), Pathname.new("#{__dir__}/../fixtures/assets/first_path")]
1124
config.output_path = Pathname.new("#{__dir__}../fixtures/output")
25+
config.prefix = "/assets"
1226
})
1327

28+
@rack_app = RackApp.new
1429
@assembly.compilers.register "text/css", Propshaft::Compiler::CssAssetUrls
15-
@server = Propshaft::Server.new(@assembly)
30+
@server = Propshaft::Server.new(@rack_app, @assembly)
31+
end
32+
33+
test "forward requests not under prefix" do
34+
get "/test"
35+
assert_not_empty @rack_app.calls
36+
end
37+
38+
test "forward requests that aren't GET or HEAD" do
39+
asset = @assembly.load_path.find("foobar/source/test.css")
40+
post "/assets/#{asset.digested_path}"
41+
assert_not_empty @rack_app.calls
1642
end
1743

1844
test "serve a compiled file" do
1945
asset = @assembly.load_path.find("foobar/source/test.css")
20-
get "/#{asset.digested_path}"
46+
get "/assets/#{asset.digested_path}"
2147

2248
assert_equal 200, last_response.status
23-
assert_equal "62", last_response.headers['content-length']
49+
assert_equal last_response.body.bytesize.to_s, last_response.headers['content-length']
2450
assert_equal "text/css", last_response.headers['content-type']
2551
assert_equal "Accept-Encoding", last_response.headers['vary']
2652
assert_equal "\"#{asset.digest}\"", last_response.headers['etag']
2753
assert_equal "public, max-age=31536000, immutable", last_response.headers['cache-control']
28-
assert_equal ".hero { background: url(\"/foobar/source/file-3e6a1297.jpg\") }\n",
54+
assert_equal ".hero { background: url(\"/assets/foobar/source/file-3e6a1297.jpg\") }\n",
2955
last_response.body
3056
end
3157

3258
test "serve a predigested file" do
3359
asset = @assembly.load_path.find("file-already-abcdefVWXYZ0123456789_-.digested.css")
34-
get "/#{asset.digested_path}"
60+
get "/assets/#{asset.digested_path}"
3561
assert_equal 200, last_response.status
3662
end
3763

3864
test "serve a sourcemap" do
3965
asset = @assembly.load_path.find("file-is-a-sourcemap.js.map")
40-
get "/#{asset.digested_path}"
66+
get "/assets/#{asset.digested_path}"
4167
assert_equal 200, last_response.status
4268
end
4369

4470
test "not found" do
45-
get "/not-found.js"
71+
get "/assets/not-found.js"
4672

4773
assert_equal 404, last_response.status
4874
assert_equal "9", last_response.headers['content-length']
@@ -55,7 +81,7 @@ class Propshaft::ServerTest < ActiveSupport::TestCase
5581

5682
test "not found if digest does not match" do
5783
asset = @assembly.load_path.find("foobar/source/test.css")
58-
get "/#{asset.logical_path}"
84+
get "/assets/#{asset.logical_path}"
5985
assert_equal 404, last_response.status
6086
end
6187

0 commit comments

Comments
 (0)