Skip to content

3.2.13: expires_in writes to tmp/cache unexpectedly; loses 'max-age' setting #10360

Closed
armhold opened this Issue Apr 26, 2013 · 13 comments

6 participants

@armhold
armhold commented Apr 26, 2013

First, I apologize for the length of this report, and also the fact that I probably have some misapprehensions about how Rails caching works, or is intended to work. I've done my best to simplify this.

I'm trying to accomplish two kinds of caching:

  1. cache common/expensive actions server-side via caches_action
  2. cache resulting HTML on the client-side by setting "Cache-Control: max-age=X" headers

The problems I ran into are that:

1) The documentation for ActionController::ConditionalGet#expires_in is unclear in that it has a rather significant side effect. It says that it "Sets a HTTP 1.1 Cache-Control header". Not mentioned is the fact that it seems to write to tmp/cache when you have 'public: true'. I expected this to only set a client-side header, so the fact that it causes server-side caching in Rails itself is surprising. Others have also apparently tripped over this (see comment at http://apidock.com/rails/ActionController/ConditionalGet/expires_in).

2) expires_in seems to use the URL as a key when writing to the cache. Not terribly surprising, but if you have a significant amount of traffic that arrives at a given action via unique URLs, this will very rapidly blow up your cache with useless entries. A common example is the 'gclid' that Adsense uses- it's unique for every visitor, so Rails will create a new cache entry for each visitor to the (otherwise identical) page. I understand that Rails can't magically know which params should be ignored, but read on.

If you're using disk-based caching this causes re-deploys to take forever (because all the disk files have to be cleaned up). Worse- if you're using Redis or Memcache, these cache entries will suck up RAM until they expire. This is kind of frustrating if all you're trying to do is get client-side caching going.

If expires_in absolutely has to write to the cache it would be nice to have a "cache_path" setting (like caches_action has):

:cache_path => Proc.new { |c| c.params.except('gclid') }

This would allow some amount of control over which params should "matter" for deciding uniqueness of a given URL.

3) If you use expires_in with 'public: false' (this seems to prevent the cache from being written on the server), and also use caches_action, all requests after the first one seem to lose the max-age setting. It's unclear to me where exactly it's being lost, and perhaps I'm doing something dumb:

caches_action :index,
              :expires_in => 10.minutes,
              :cache_path => Proc.new { |c| c.params.except('gclid') }

def index
    expires_in 10.minutes, public: false
end

Now try hitting that URL and watch the headers that come back:

$ curl -I 'http://localhost:3000'
HTTP/1.1 200 OK 
Content-Type: text/html; charset=utf-8
Cache-Control: max-age=600, private          <-- what we want to see
X-Ua-Compatible: IE=Edge
Etag: "e7cfee1d01bd32e0f1bc44dd53173b09"
X-Request-Id: ced4ff73793cf43e3c285d999967328f
X-Runtime: 0.013361
Date: Fri, 26 Apr 2013 14:20:30 GMT
X-Rack-Cache: miss
Content-Length: 0
Server: WEBrick/1.3.1 (Ruby/1.9.3/2012-12-25)
Connection: Keep-Alive
Set-Cookie: _caching_session=BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJTMwYTMzNDZlYTIzNDQ3NGQwNWNkMmM5ODIzYjI0MTFjBjsAVEkiEF9jc3JmX3Rva2VuBjsARkkiMTJZdXlkdmVVa2tkY1dWblI0eU04ZVVTT2FvMEpBWFhwUzVmSGMreEFPUk09BjsARg%3D%3D--2c2ee286672ef8d02531f18f06e6107e655482ce; path=/; HttpOnly

$ curl -I 'http://localhost:3000'
HTTP/1.1 200 OK 
Content-Type: text/html; charset=utf-8
X-Ua-Compatible: IE=Edge
Etag: "e7cfee1d01bd32e0f1bc44dd53173b09"
Cache-Control: max-age=0, private, must-revalidate  <-- oops, that's not helpful
X-Request-Id: 2818b738752ceab0404d875e3f5df1e9
X-Runtime: 0.006367
Date: Fri, 26 Apr 2013 14:20:33 GMT
X-Rack-Cache: miss
Content-Length: 0
Server: WEBrick/1.3.1 (Ruby/1.9.3/2012-12-25)
Connection: Keep-Alive

Note that you will need to set config.action_controller.perform_caching = true in development.rb to test this in dev. Again, apologies if I'm doing something dumb here; enlightenment welcomed.

Thanks for your attention.

@inspire22

+1 This "crazy" behavior made me disable the rack::cache middleware immediately after it launched

This seems to have the unfortunate side effect of making it impossible to set browser cache times via max_age / expires_in (it's just ignored)

To disable rack cache, in application.rb, include:
config.middleware.delete Rack::Cache

@rafaelfranca
Ruby on Rails member

This only occurs on 3.2.13 or any 3.2.x version?

@inspire22
@rafaelfranca
Ruby on Rails member

Ok. Thank you for confirming. This means it is not a regression.

@inspire22

After more investigation, the culprit for my problem with max-age not being set was my nginx configuration having

expires 0;

Which I thought would be a default-unless-set, but it overwrites any expires header from the backend.

I was sure this wasn't the culprit because expires-caching was working on my production server - until I realized it had broken some time ago.

@armhold
armhold commented May 21, 2013

FWIW the 'curl' tests I mentioned above produce the problem in development mode, without any web server fronting Rails.

@steveklabnik
Ruby on Rails member

Technically Webrick is the web server fronting Rails in that situation.

@armhold
armhold commented May 21, 2013

Sorry; what I meant was that there was no apache/nginx config getting in the way. Steve is indeed correct, but I get the same behavior for example when I run 'thin' instead of webrick.

@inspire22

Right, sorry to be confusing - I was referring only to my additional complaint in the comments about it breaking the max-age setting (it doesn't - it was my nginx)

@guilleiguaran
Ruby on Rails member

This is happening also when Rack::Cache is disabled?

@armhold
armhold commented Dec 20, 2013

Hi, apologies for the crazy delay, but I believe I have boiled this down to a simple project and accompanying testcase.

This project uses Rails 3.2.16; it also behaves similarly in 4.0.2 with the actionpack-action_caching gem added.

It shows that if you use caches_action along with expires_in, your first response will have a proper Cache-Control header, but subsequent (cached) responses will not. The upshot is that you cannot both cache actions and also ask clients to cache the result.

Thanks again for your time and attention.

@armhold armhold added the stale label Apr 23, 2014
@rafaelfranca
Ruby on Rails member

This issue has been automatically marked as stale because it has not been commented on for at least
three months.

The resources of the Rails team are limited, and so we are asking for your help.

If you can still reproduce this error on the 4-1-stable, 4-0-stable branches or on master,
please reply with all of the information you have about it in order to keep the issue open.

Thank you for all your contributions.

@rails-bot rails-bot closed this May 27, 2014
@rails-bot

This issue has been automatically closed because of inactivity.

If you can still reproduce this error on the 4-1-stable, 4-0-stable branches or on master,
please reply with all of the information you have about it in order to keep the issue open.

Thank you for all your contributions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.