Skip to content


Subversion checkout URL

You can clone with
Download ZIP


Allow Rack::File and Rack::Static to serve custom HTTP Headers #417

merged 2 commits into from

3 participants


Hey everyone,

this PR makes Rack::File and Rack::Static able to serve custom HTTP Headers.

# Rack::Static
use Rack::Static, :root => 'public', :headers =>  {'Cache-Control' => 'public, max-age=31536000', 'Access-Control-Allow-Origin' => '*'}

# Plain Rack::File
@file_server =, headers)

use Rack::Static, :root => 'public', :cache_control => 'public, max-age=31536000'
@file_server =, cache_control)

The API changes all respect backward compatibility, all existing and new tests pass. It's implemented in a way that allows the all legacy implementations to continue but give priority to the settings provided in :headers => {}.

What's the reason behind this?
When deploying a Rails app the to date usual way to handle the serving of static files is by having a nginx instance or similar running in front of your Rails app. This nginx instance will set custom HTTP Headers for files it serves based on rules you define. The served files including the custom HTTP Headers can be cached by a CDN like Amazon's Cloudfront and then served to the website visitor.

When deploying to Heroku's Cedar stack there is no nginx etc. instance in front that could help with setting custom HTTP headers for files. Heroku currently enables Rails to serve static files through the ActionDispatch::Static middleware, which is enables by a plugin injection of theirs. ActionDispatch::Static relies on Rack::File for serving the precompiled assets. The served files can once again be cached by Amazon's Cloudfront, they need to carry all the headers you want them to carry however when sending them from your app, and I think there is no way to add custom headers via Cloudfront (even if there were, this wouldn't be the place to set them, would it?).

To have a file served by any higher level static files webserver with custom headers the basic Rack::File implementation should be able to add custom HTTP headers wider than 'Cache-Control' a developer wants the served files to carry. Rack::Static as well as ActionDispatch::Static both rely on this implementation.

Is it nescessary to serve files with custom headers at all?
Yes. There's an issue with web fonts / icon fonts and firefox that require icon fonts to carry a header of 'Access-Control-Allow-Origin' = '*' or set to your domain name. Currently this is a huge pain in the ass especially for Rails developers deploying their app to Heroku and shipping their assets via a CDN.

Unless a functionality such as the one proposed in this PR is being implemented, Rack::File will not be able to serve web fonts that can be delivered by a CDN in a way that Firefox will render them. The required HTTP headers are simply missing.

Thanks for reading this rather long explanation. If you'd like me to point you to sources for anything stated above please comment, I'll try to dig out the relevant stuff for you from the many small clues you get when researching this topic.

The longer term goal here might be to have a great ruby static files server implementation that can be used to serve static files in production. It would be great to see this extended in the future to allow developers to add headers to files in a certain folder or ones that match certain Regexp expressions.

What do you think? Which way, at which place, would you set custom headers?



This pull request passes (merged beddb2a into ab67e70).


This pull request fails (merged bc60422 into ab67e70).

@raggi raggi merged commit b88b678 into rack:master

1 check failed

Details default The Travis build failed

Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
13 lib/rack/file.rb
@@ -21,9 +21,14 @@ class File
alias :to_path :path
- def initialize(root, cache_control = nil)
+ def initialize(root, headers={})
@root = root
- @cache_control = cache_control
+ # Allow a cache_control string for backwards compatibility
+ if headers.instance_of? String
+ @headers = { 'Cache-Control' => headers }
+ else
+ @headers = headers
+ end
def call(env)
@@ -78,7 +83,9 @@ def serving(env)
env["REQUEST_METHOD"] == "HEAD" ? [] : self
- response[1]['Cache-Control'] = @cache_control if @cache_control
+ # Set custom headers
+ @headers.each { |field, content| response[1][field] = content } if @headers
# We check via File::size? whether this file provides size info
17 lib/rack/static.rb
@@ -32,6 +32,16 @@ module Rack
# use Rack::Static, :root => 'public', :cache_control => 'public'
+ # Set custom HTTP Headers for all served files:
+ #
+ # use Rack::Static, :root => 'public', :headers =>
+ # {'Cache-Control' => 'public, max-age=31536000',
+ # 'Access-Control-Allow-Origin' => '*'}
+ #
+ # Note: If both :headers => {'Cache-Control' => 'public, max-age=42'}
+ # and :cache_control => 'public, max-age=38' are being provided
+ # the :headers setting takes precedence
+ #
class Static
@@ -40,8 +50,11 @@ def initialize(app, options={})
@urls = options[:urls] || ["/favicon.ico"]
@index = options[:index]
root = options[:root] || Dir.pwd
- cache_control = options[:cache_control]
- @file_server =, cache_control)
+ headers = options[:headers] || {}
+ # Allow for legacy :cache_control option
+ # while prioritizing :headers => {'Cache-Control' => ''} settings
+ headers['Cache-Control'] ||= options[:cache_control] if options[:cache_control]
+ @file_server =, headers)
def overwrite_file_path(path)
21 test/spec_file.rb
@@ -145,7 +145,7 @@ def file(*args)
res["Content-Range"].should.equal "bytes */193"
- should "support cache control options" do
+ should "support legacy cache control options provided as string" do
env = Rack::MockRequest.env_for("/cgi/test")
status, heads, _ = file(DOCROOT, 'public, max-age=38').call(env)
@@ -153,6 +153,25 @@ def file(*args)
heads['Cache-Control'].should.equal 'public, max-age=38'
+ should "support custom http headers" do
+ env = Rack::MockRequest.env_for("/cgi/test")
+ status, heads, _ = file(DOCROOT, 'Cache-Control' => 'public, max-age=38',
+ 'Access-Control-Allow-Origin' => '*').call(env)
+ status.should.equal 200
+ heads['Cache-Control'].should.equal 'public, max-age=38'
+ heads['Access-Control-Allow-Origin'].should.equal '*'
+ end
+ should "support not add custom http headers if none are supplied" do
+ env = Rack::MockRequest.env_for("/cgi/test")
+ status, heads, _ = file(DOCROOT).call(env)
+ status.should.equal 200
+ heads['Cache-Control'].should.equal nil
+ heads['Access-Control-Allow-Origin'].should.equal nil
+ end
should "only support GET and HEAD requests" do
req =
21 test/spec_static.rb
@@ -12,7 +12,7 @@ def call(env)
def static(app, *args), *args)
root = File.expand_path(File.dirname(__FILE__))
OPTIONS = {:urls => ["/cgi"], :root => root}
@@ -78,4 +78,23 @@ def static(app, *args)
res.headers['Cache-Control'].should == 'public'
+ it "supports serving custom http headers" do
+ opts = OPTIONS.merge(:headers => {'Cache-Control' => 'public, max-age=42',
+ 'Access-Control-Allow-Origin' => '*'})
+ request =, opts))
+ res = request.get("/cgi/test")
+ res.headers['Cache-Control'].should == 'public, max-age=42'
+ res.headers['Access-Control-Allow-Origin'].should == '*'
+ end
+ it "allows headers hash to take priority over fixed cache-control" do
+ opts = OPTIONS.merge(:cache_control => 'public, max-age=38',
+ :headers => {'Cache-Control' => 'public, max-age=42'})
+ request =, opts))
+ res = request.get("/cgi/test")
+ res.headers['Cache-Control'].should == 'public, max-age=42'
+ end
Something went wrong with that request. Please try again.