How to set Cache-control for ActiveStorage Disk service? - ruby-on-rails

I have a simple Rails 6 app with ActiveStorage. I use local disk storage. When I inspect responses from representation url like this
http://localhost:3000/rails/active_storage/disk/some-long-hash/IMG_0951.jpeg?content_type=image%2Fjpeg&disposition=inline%3B+filename%3D%22IMG_0951.jpeg%22%3B+filename%2A%3DUTF-8%27%27IMG_0951.jpeg
I see headers Cache-Control: max-age=0, private, must-revalidate
The question is how to make Rails to set public caching header with some age?

The #show method for ActiveStorage::DiskController is difficult to override but it can be done.
A simpler approach is to add an after_action callback for the existing #show method to insert the Cache-Control header when it is called:
# config/initializers/active_storage.rb
require 'active_storage/current'
ActiveStorage::Current.url_options = { host: 'localhost', port: 3000 }
require 'active_storage/set_current'
require 'active_storage/base_controller'
require 'active_storage/file_server'
require 'active_storage/disk_controller'
class ActiveStorage::DiskController < ActiveStorage::BaseController
after_action do
response.set_header('Cache-Control', 'max-age=3600, public') if action_name == 'show'
end
end
Requesting an ActiveStorage URL then returns the custom Cache-Control header value in the response:
HTTP/1.1 200 OK
Cache-Control: max-age=3600, public
...

Related

Rspec test for GZIPed response

I recently enabled GZIP on my Rails 4 app following this Thoughtbot blog post and I also have added use Rack::Deflater to my config.ru file as suggested by this post. My Rails app seems to be serving compressed content, but when I test for it using RSpec the test fails because response.headers['Content-Encoding'] is nil.
Here is my application.rb:
module MyApp
class Application < Rails::Application
# Turn on GZIP compression
config.middleware.use Rack::Deflater
end
end
Here is my spec:
require 'rails_helper'
describe GeneralController, type: :controller, focus: true do
it "a visitor has a browser that supports compression" do
['deflate', 'gzip', 'deflate,gzip', 'gzip,deflate'].each do |compression_method|
get 'about', {}, {'HTTP_ACCEPT_ENCODING' => compression_method }
binding.pry
expect(response.headers['Content-Encoding']).to be
end
end
it "a visitor's browser does not support compression" do
get 'about'
expect(response.headers['Content-Encoding']).to_not be
end
end
When I run curl --head -H "Accept-Encoding: gzip" http://localhost:3000/ I get the following output:
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Ua-Compatible: chrome=1
Content-Type: text/html; charset=utf-8
Vary: Accept-Encoding
Content-Encoding: gzip
Etag: "f7e364f21dbb81b9580cd39e308a7c15"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 3f018f27-40ab-4a87-a836-67fdd6bd5b6e
X-Runtime: 0.067748
Server: WEBrick/1.3.1 (Ruby/2.0.0/2014-02-24)
When I load the site and look at the Network tab of the inspector I can see that the response size is smaller than before, but my test still fails. I'm not sure if I'm missing a step here with my test or if there is an issue with my implementation of Rack::Deflater.
As #andy-waite pointed, RSpec controller specs are not aware of middleware, but that's why, since RSpec 2.6 we have request specs.
Request specs are, according to the docs:
designed to drive behavior through the full stack
Therefore, using RSpec > 2.6 request specs, your code should look like:
require 'rails_helper'
describe GeneralController, type: :request, focus: true do
it "a visitor has a browser that supports compression" do
['deflate', 'gzip', 'deflate,gzip', 'gzip,deflate'].each do |compression_method|
get 'about', {}, {'HTTP_ACCEPT_ENCODING' => compression_method }
binding.pry
expect(response.headers['Content-Encoding']).to be
end
end
it "a visitor's browser does not support compression" do
get 'about'
expect(response.headers['Content-Encoding']).to_not be
end
end
RSpec controller specs are wrapped around Rails functional tests, which are not aware of middleware:
Making Rails tests aware of Rack middleware outside Rails's internal chain

Ember app that consumes json getting error "XMLHttpRequest cannot load"

I have a rails app on heroku that is serving up data via an API. This data looks like...
[{"created_at":"2014-02-20T17:22:02Z","id":1,"name":"Joe Rogan","twitter":"#joerogan","updated_at":"2014-02-20T17:22:02Z"},{"created_at":"2014-02-20T17:22:11Z","id":2,"name":"Kristen Schaa","twitter":"#kchalithis","updated_at":"2014-02-20T17:22:11Z"},{"created_at":"2014-02-20T17:29:10Z","id":3,"name":"Casey Grim","twitter":"#aCoupleofN3rds","updated_at":"2014-02-20T17:29:16Z"}]
I pulled down the Ember Start Kit and have modified it to pull data from my API with code like this...
js/app.js
App.Person.adapter = Ember.RESTAdapter.create();
App.Person.url = "http://myapp.herokuapp.com/api/v1/shots?api_key=12d2d06fb2f6a786ac75b32625cf83a1";
App.Person.collectionKey = "people";
index.html
<script type="text/x-handlebars" id="index">
<ul>
{{#each item in model}}
<li>{{item.name}}</li>
{{/each}}
</ul>
</script>
I thought I could start chrome like this...
google-chrome --disable-web-security
And get it working just for testing, but that does not work.
Also, Sounds like I need to use jsonp rather than json? But not sure how to implement in ember (or really anywhere else). Any help appreciated. Thanx!
Update
Added :cors_set_access_control_headers before filter in my rails app. looks like...
module Api
module V1
class ShotsController < ApplicationController
before_filter :cors_set_access_control_headers
...
def cors_set_access_control_headers
headers['Access-Control-Allow-Origin'] = '*'
headers['Access-Control-Allow-Methods'] = 'POST, PUT, DELETE, GET, OPTIONS'
headers['Access-Control-Request-Method'] = '*'
headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Authorization'
end
...but still getting error
XMLHttpRequest cannot /api/v1/shots?api_key=d26da3938adc5f3c8604256194c18501.json. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access.
...if I issue a curl to look at headers I'm seeing Access Controll set to *...
me#me-E6530:~$ curl -v http://myapp.herokuapp.com/api/v1/shots?api_key=12d2d06fb2f6a786ac75b32625cf83a1
* About to connect() to myapp.herokuapp.com port 80 (#0)
* Trying 54.243.169.60... connected
> GET /api/v1/shots?api_key=12d2d06fb2f6a786ac75b32625cf83a1 HTTP/1.1
> User-Agent: curl/7.22.0 (i686-pc-linux-gnu) libcurl/7.22.0 OpenSSL/1.0.1 zlib/1.2.3.4 libidn/1.23 librtmp/2.3
> Host: myapp.herokuapp.com
> Accept: */*
>
< HTTP/1.1 200 OK
< Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization
< Access-Control-Allow-Methods: POST, PUT, DELETE, GET, OPTIONS
< Access-Control-Allow-Origin: *
< Access-Control-Request-Method: *
Ember expects response in the following format:
{
people: [
{
id: 1,
name: "John Doe"
},
{
id: 2,
name: "Jane Doe"
}
]
}
Also you may need to enable CORS if they are on different domains.

Rails 3.2 Rack::Cache HTTP Headers and Action Caching

Good afternoon,
I've run into some issues trying to combine HTTP caching with Rack::Cache and action caching (on my Heroku-hosted app).
Using them individually, it seems to be working. With action caching enabled, the page loading is snappy, and the log would suggest it is caching. With HTTP caching in the controllers (eTag, last_modified and fresh_when) the proper headers appear to be set.
However, when I try to combine the two, it appears to be action caching, but the HTTP headers are always max_age: 0, must_revalidate. Why is this? Am I doing something wrong?
For example, here's the code in my "home" action:
class StaticPagesController < ApplicationController
layout 'public'
caches_action :about, :contact, ......, :home, .....
......
def home
last_modified = File.mtime("#{Rails.root}/app/views/static_pages/home.html.haml")
fresh_when last_modified: last_modified , public: true, etag: last_modified
expires_in 10.seconds, :public => true
end
For all intents and purposes, this should have a public cache-control tag with max-age 10 no?
$ curl -I http://myapp-staging.herokuapp.com/
HTTP/1.1 200 OK
Cache-Control: max-age=0, private, must-revalidate
Content-Type: text/html; charset=utf-8
Date: Thu, 24 May 2012 06:50:45 GMT
Etag: "997dacac05aa4c73f5a6861c9f5a9db0"
Status: 200 OK
Vary: Accept-Encoding
X-Rack-Cache: stale, invalid
X-Request-Id: 078d86423f234da1ac41b418825618c2
X-Runtime: 0.005902
X-Ua-Compatible: IE=Edge,chrome=1
Connection: keep-alive
Config Info:
# Use a different cache store in production
config.cache_store = :dalli_store
config.action_dispatch.rack_cache = {
:verbose => true,
:metastore => "memcached://#{ENV['MEMCACHE_SERVERS']}",
:entitystore => "memcached://#{ENV['MEMCACHE_SERVERS']}"#,
}
In my mind, you should be able to use action caching as well as a reverse proxy correct? I know that they do fairly similar things (if the page changes, both the proxy and the action cache will be invalid and need to be regenerated), but I feel I should be able to have both in there. Or should I get rid of one?
UPDATE
Thanks for the answer below! It seems to work. But to avoid having to write set_XXX_cache_header methods for every controller action, do you see any reason why this wouldn't work?
before_filter :set_http_cache_headers
.....
def set_http_cache_headers
expires_in 10.seconds, :public => true
last_modified = File.mtime("#{Rails.root}/app/views/static_pages/#{params[:action]}.html.haml")
fresh_when last_modified: last_modified , public: true, etag: last_modified
end
When you use action caching, only the response body and content type is cached. Any other changes to the response will not happen on subsequent requests.
However, action caching will run any before filters even when the action itself is cached.
So, you can do something like this:
class StaticPagesController < ApplicationController
layout 'public'
before_filter :set_home_cache_headers, :only => [:home]
caches_action :about, :contact, ......, :home, .....
......
def set_home_cache_headers
last_modified = File.mtime("#{Rails.root}/app/views/static_pages/home.html.haml")
fresh_when last_modified: last_modified , public: true, etag: last_modified
expires_in 10.seconds, public: true
end

How do I set Expires: header when using send_data

I have a method in my controller which uses send_data like this:
def show
expires_in 10.hours, :public => true
send_data my_image_generator, :filename => "image.gif", :type => "image/gif"
end
Using expires_in results in headers being sent like this:
HTTP/1.1 200 OK
Connection: close
Date: Fri, 25 Jun 2010 10:41:22 GMT
ETag: "885d75258e9306c46a5dbfe3de44e581"
Content-Transfer-Encoding: binary
X-Runtime: 143
Content-Type: image/gif
Content-Disposition: inline; filename="image.gif"
Content-Length: 1277
Cache-Control: max-age=36000, public
What I would like to do is add an header like Expires: (some exact date) to keep the user agent from revalidating. But I don't see how to make send_data set that header?
I guess I could set it explicitly in the response.headers hash, but surely there must be a wrapper for that (or something)?
I came across this syntax and I like it :-)
response.headers["Expires"] = 1.year.from_now.httpdate
Apparently there seems to be no way to pass expires to send_data - instead you must set it yourself in response.headers and take care of formatting the date appropriately:
response.headers["Expires"] = CGI.rfc1123_date(Time.now + period)
Note that the max-age directive in the Cache-Control header overrides the Expires header if both are present. See RFC2616 Section 14.9.3 for more details.
The code in your question should actually work on recent Rails:
`expires_in 10.hours, :public => true`

Mongrel::DirHandler equivalent for Passenger

I'm using Mongrel::DirHandler to control response headers for static files - this works great on my dev machine. My production machine uses Passenger so my headers aren't getting set. How do I control headers for static files when using Passenger?
snippet from my environment.rb:
if defined? Mongrel::DirHandler
module Mongrel
class DirHandler
def send_file_with_expires(req_path, request, response, header_only=false)
if req_path =~ /((\/images)|javascripts|stylesheets)/
response.header['Cache-Control'] = 'max-age=315360000'
response.header['Expires'] = (Time.now + 10.years).rfc2822
else
response.header["Last-Modified"] = Time.now.httpdate
response.header["Expires"] = 0
# HTTP 1.0
response.header["Pragma"] = 'no-cache'
# HTTP 1.1 ‘pre-check=0, post-check=0′ (IE specific)
response.header["Cache-Control"] = 'no-store, no-cache, must-revalidate, max-age=0, pre-check=0, post-check=0'
end
send_file_without_expires(req_path, request, response, header_only)
end
alias_method :send_file_without_expires, :send_file
alias_method :send_file, :send_file_with_expires
end
end
end
Since you're using Passenger, I assume you're under apache, so your request isn't going through Mongrel anymore. If so, you can establish rules on the .htaccess file inside the public directory of your application.
Here's an explination on how to do it.

Resources