diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index 03344db8d2..ea300badec 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -541,13 +541,40 @@ module ActionView end end - def send_preload_links_header(preload_links) + MAX_HEADER_SIZE = 8_000 # Some HTTP client and proxies have a 8kiB header limit + def send_preload_links_header(preload_links, max_header_size: MAX_HEADER_SIZE) if respond_to?(:request) && request request.send_early_hints("Link" => preload_links.join("\n")) end if respond_to?(:response) && response - response.headers["Link"] = [response.headers["Link"].presence, *preload_links].compact.join(",") + header = response.headers["Link"] + header = header ? header.dup : +"" + + # rindex count characters not bytes, but we assume non-ascii characters + # are rare in urls, and we have a 192 bytes margin. + last_line_offset = header.rindex("\n") + last_line_size = if last_line_offset + header.bytesize - last_line_offset + else + header.bytesize + end + + preload_links.each do |link| + if link.bytesize + last_line_size + 1 < max_header_size + unless header.empty? + header << "," + last_line_size += 1 + end + else + header << "\n" + last_line_size = 0 + end + header << link + last_line_size += link.bytesize + end + + response.headers["Link"] = header end end end diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index 827164cda4..67ac309fb1 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -537,6 +537,17 @@ class AssetTagHelperTest < ActionView::TestCase end end + def test_should_generate_links_under_the_max_size + with_preload_links_header do + 100.times do |i| + stylesheet_link_tag("http://example.com/style.css?#{i}") + javascript_include_tag("http://example.com/all.js?#{i}") + end + lines = @response.headers["Link"].split("\n") + assert_equal 2, lines.size + end + end + def test_should_not_preload_links_with_defer with_preload_links_header do javascript_include_tag("http://example.com/all.js", defer: true)