Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Site doesn't ever return a 304 #354

Open
natematykiewicz opened this issue Apr 3, 2020 · 9 comments
Open

Site doesn't ever return a 304 #354

natematykiewicz opened this issue Apr 3, 2020 · 9 comments

Comments

@natematykiewicz
Copy link
Contributor

The response headers don't include an ETag, so my browser can't send a If-None-Match, so that the server can return a 304.

Screen Shot 2020-04-02 at 10 08 20 PM

I thought that the cf-cache-status: DYNAMIC seemed suspicious.

https://support.cloudflare.com/hc/en-us/articles/200172516-Which-file-extensions-does-CloudFlare-cache-for-static-content-

DYNAMIC
The resource was not cached by default and your current Cloudflare caching configuration doesn't instruct Cloudflare to cache the resource.  Instead, the resource was requested from the origin web server. Use Page Rules to implement custom caching options.

I don't know much about Cloudflare, unfortunately.

@natematykiewicz
Copy link
Contributor Author

@ioquatix do you think this is still a relevant issue?

@ioquatix
Copy link
Collaborator

An we confirm if-none-match is working?

@natematykiewicz
Copy link
Contributor Author

@ioquatix On localhost, the server returns an ETag, but my browser isn't sending If-None-Match in a subsequent request.

Screen Shot 2020-04-18 at 9 54 11 PM

Prod does not even return an ETag.

Screen Shot 2020-04-18 at 9 54 29 PM

Do we care? I feel like ideally this would work.

@ioquatix
Copy link
Collaborator

It should work, but maybe cloudflare is doing something weird.

@ioquatix
Copy link
Collaborator

Soooooo....

#!/usr/bin/env ruby

require 'async'
require 'async/http/endpoint'
require 'async/http/client'

Async do |task|
	endpoint = Async::HTTP::Endpoint.parse("https://rubyapi-org.herokuapp.com/2.7/o/string")
	client = Async::HTTP::Client.new(endpoint)
	
	response = client.get(endpoint.path, {
		'if-none-match' => 'W/"578a945b0772ae259625a9e66f06cdff"'
	})
	
	Async.logger.info(response, name: "headers") do |buffer|
		response.headers.each do |key, value|
			buffer.puts "#{key.rjust(40)}: #{value}"
		end
	end
	
	body = response.read
	
	Async.logger.info(response) {"Status: #{response.status} Body: #{body&.bytesize.inspect} bytes"}
	
	Async.logger.info(response, name: "trailers") do |buffer|
		response.headers.trailers.each do |key, value|
			buffer.puts "#{key.rjust(40)}: #{value}"
		end
	end
end

Does seem to work correctly (returns 304).

However, it's not the same etag as generated by async-http-cache. Because I only generate strong etags and use sha256 which is like 2x the length. Maybe it's Rack::ETag middleware. However, if-none-match still seems fine. But for some reason, cloudflare is not honouring it.

If I set:

	endpoint = Async::HTTP::Endpoint.parse("https://rubyapi.org/2.7/o/string")

...there is no etag as you point out and also if-none-match doesn't work (using the upstream etag).

Is this some kind of feature/function of cloudflare?

@ioquatix
Copy link
Collaborator

So what I can say is that it seems like the LB at Heroku is doing the right thing but Cloudflare is doing something different.

@natematykiewicz
Copy link
Contributor Author

@colby-swandale @nateberkopec, any ideas?

@ioquatix
Copy link
Collaborator

https://www.mnot.net/blog/2007/08/07/etags

Reading through this, it seems to imply that if you set cache-control: max-age=x that you don't need etags since the cache is fresh until it expires.

@natematykiewicz
Copy link
Contributor Author

Hmm. My computer still does a web request even though Cache-Control: public, max-age=86400 is being specified in the response.

That article seems to be saying that if the content doesn't change, give the response a max-age. Ruby API (like most Rails apps) is one where content can change. A predetermined cache expiration is not necessarily correct. If Colby does a deployment, the content that's returned can change pretty dramatically, and also the asset URLs can change. If he's purging old assets (which he probably is) on a deployment, you can end up with stale asset URLs being returned from the cached HTML (which I saw once like 2 weeks ago). I think this is generally why Rails prefers the ETag approach.

Here's the pros / cons list for Ruby API as I see it:

max-age

pros

  • Avoid a network call
  • Reduces server load, because a web request is not made (meaning less $$$ to Heroku)
  • Fastest possible page load

cons

  • Stale cache can lead to inconsistencies between pages
  • Stale cache can lead to 404s on assets
  • Stale cache can lead to a new feature or bug fix not being actually released right a way (not a big deal most times, but if you really messed up a deployment, it would stink for someone to cache a huge mistake for 24 hours)

ETag

pros

  • Always accurate content
  • Bug fixes and features are immediate
  • More efficient than always returning a 200

cons

  • Requires a network request
  • Does not reduce server load because requests are not skipped (so no cost savings)
  • Requires the server to build the entire response, just to figure out you already have the correct content

I feel like max-age is nice for truly static content. Like at my job, we generate a unique URL for uploaded photos. The data that the URL returns literally will never change. This is a great time to use max-age.

For content that's static a lot of the time but can change (during deployments, or data reimports), I don't know if max-age makes sense.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants