Skip to content

Commit

Permalink
Do not set a Last-Modified header
Browse files Browse the repository at this point in the history
The Last-Modified header is a weaker cache key than the etag, and thus
we rely solely on the etag from now on. Browsers and Rails will ignore
it if it's not set while validating if a request is fresh.
  • Loading branch information
mamhoff committed May 13, 2024
1 parent 7b1d334 commit 9343574
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 14 deletions.
2 changes: 1 addition & 1 deletion app/controllers/alchemy/json_api/admin/pages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def set_current_preview
end
end

def last_modified_for(page)
def page_cache_key(page)
page.updated_at
end

Expand Down
21 changes: 18 additions & 3 deletions app/controllers/alchemy/json_api/pages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@
module Alchemy
module JsonApi
class PagesController < JsonApi::BaseController
JSONAPI_STALEMAKERS = %i[include fields sort filter page]

before_action :load_page_for_cache_key, only: :show

def index
allowed = Alchemy::Page.ransackable_attributes

jsonapi_filter(page_scope, allowed) do |filtered_pages|
@pages = filtered_pages.result

if !@pages.all?(&:cache_page?)
render_pages_json(allowed) && return
elsif stale?(last_modified: @pages.maximum(:published_at), etag: @pages.max_by(&:cache_key_with_version)&.cache_key_with_version)
elsif stale?(etag: etag(@pages))
render_pages_json(allowed)
end
end
Expand All @@ -25,7 +28,7 @@ def show
render(jsonapi: api_page(load_page)) && return
end

if stale?(last_modified: last_modified_for(@page), etag: @page.cache_key_with_version)
if stale?(etag: etag(@page))
# Only load page with all includes when browser cache is stale
render jsonapi: api_page(load_page)
end
Expand Down Expand Up @@ -61,7 +64,8 @@ def load_page_for_cache_key
end

def last_modified_for(page)
page.published_at
return unless page
page.respond_to?(:last_modified_at) ? page.last_modified_at : page.published_at
end

def jsonapi_meta(pages)
Expand Down Expand Up @@ -117,6 +121,17 @@ def api_page(page)
Alchemy::JsonApi::Page.new(page, page_version_type: page_version_type)
end

def etag(pages)
pages = Array.wrap(pages)
return unless pages.any?
relevant_params = params.to_unsafe_hash.slice(*JSONAPI_STALEMAKERS).values.compact
pages.map { |page| page_cache_key(page) }.concat(relevant_params)
end

def page_cache_key(page)
page.cache_key_with_version
end

def base_page_scope
# cancancan is not able to merge our complex AR scopes for logged in users
if can?(:edit_content, ::Alchemy::Page)
Expand Down
2 changes: 1 addition & 1 deletion spec/requests/alchemy/json_api/admin/layout_pages_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@

it "sets cache headers" do
get alchemy_json_api.admin_layout_page_path(page)
expect(response.headers["Last-Modified"]).to eq(page.updated_at.utc.httpdate)
expect(response.headers["Last-Modified"]).to be nil
expect(response.headers["ETag"]).to match(/W\/".+"/)
expect(response.headers["Cache-Control"]).to eq("max-age=0, private, must-revalidate")
end
Expand Down
2 changes: 1 addition & 1 deletion spec/requests/alchemy/json_api/admin/pages_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@

it "sets cache headers" do
get alchemy_json_api.admin_page_path(page)
expect(response.headers["Last-Modified"]).to eq(page.updated_at.utc.httpdate)
expect(response.headers["Last-Modified"]).to be(nil)
expect(response.headers["ETag"]).to match(/W\/".+"/)
expect(response.headers["Cache-Control"]).to eq("max-age=0, private, must-revalidate")
end
Expand Down
72 changes: 64 additions & 8 deletions spec/requests/alchemy/json_api/pages_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,32 @@

it "sets public cache headers" do
get alchemy_json_api.page_path(page)
expect(response.headers["Last-Modified"]).to eq(page.published_at.utc.httpdate)
expect(response.headers["Last-Modified"]).to be_nil
expect(response.headers["ETag"]).to eq('W/"0741fe32d81bfdabfeb47d9939c5f6b7"')
expect(response.headers["Cache-Control"]).to eq("max-age=10800, public, must-revalidate")
end

it "returns a different etag if different filters are present" do
get alchemy_json_api.page_path(page)
etag = response.headers["ETag"]
get alchemy_json_api.pages_path(filter: {page_layout_eq: "standard"})
expect(response.headers["ETag"]).not_to eq(etag)
end

it "returns a different etag if different include params are present" do
get alchemy_json_api.page_path(page)
etag = response.headers["ETag"]
get alchemy_json_api.pages_path(include: "all_elements.ingredients")
expect(response.headers["ETag"]).not_to eq(etag)
end

it "returns a different etag if different fields params are present" do
get alchemy_json_api.page_path(page)
etag = response.headers["ETag"]
get alchemy_json_api.pages_path(fields: "urlname")
expect(response.headers["ETag"]).not_to eq(etag)
end

context "if page is restricted" do
let(:page) do
FactoryBot.create(
Expand Down Expand Up @@ -178,9 +199,9 @@

describe "GET /alchemy/json_api/pages" do
context "with layoutpages and unpublished pages" do
let!(:layoutpage) { FactoryBot.create(:alchemy_page, :layoutpage, :public) }
let!(:non_public_page) { FactoryBot.create(:alchemy_page) }
let!(:public_page) { FactoryBot.create(:alchemy_page, :public, published_at: published_at) }
let!(:layoutpage) { FactoryBot.create(:alchemy_page, :layoutpage, :public, page_layout: "standard") }
let!(:non_public_page) { FactoryBot.create(:alchemy_page, page_layout: "standard") }
let!(:public_page) { FactoryBot.create(:alchemy_page, :public, published_at: published_at, page_layout: "standard") }

context "as anonymous user" do
let!(:pages) { [public_page] }
Expand All @@ -193,11 +214,46 @@

it "sets public cache headers of latest published page" do
get alchemy_json_api.pages_path
expect(response.headers["Last-Modified"]).to eq(pages.max_by(&:published_at).published_at.utc.httpdate)
expect(response.headers["Last-Modified"]).to be_nil
expect(response.headers["ETag"]).to match(/W\/".+"/)
expect(response.headers["Cache-Control"]).to eq("max-age=10800, public, must-revalidate")
end

it "returns a different etag if different filters are present" do
get alchemy_json_api.pages_path
etag = response.headers["ETag"]
get alchemy_json_api.pages_path(filter: {page_layout_eq: "standard"})
expect(response.headers["ETag"]).not_to eq(etag)
end

it "returns a different etag if different sort params are present" do
get alchemy_json_api.pages_path
etag = response.headers["ETag"]
get alchemy_json_api.pages_path(sort: {id: :asc})
expect(response.headers["ETag"]).not_to eq(etag)
end

it "returns a different etag if different include params are present" do
get alchemy_json_api.pages_path
etag = response.headers["ETag"]
get alchemy_json_api.pages_path(include: "all_elements.ingredients")
expect(response.headers["ETag"]).not_to eq(etag)
end

it "returns a different etag if different fields params are present" do
get alchemy_json_api.pages_path
etag = response.headers["ETag"]
get alchemy_json_api.pages_path(fields: "urlname")
expect(response.headers["ETag"]).not_to eq(etag)
end

it "returns a different etag if different fields params are present" do
get alchemy_json_api.pages_path
etag = response.headers["ETag"]
get alchemy_json_api.pages_path(page: {number: 2, size: 1})
expect(response.headers["ETag"]).not_to eq(etag)
end

context "if one page is restricted" do
let!(:restricted_page) do
FactoryBot.create(
Expand Down Expand Up @@ -304,10 +360,10 @@
stub_alchemy_config(:cache_pages, true)
end

it "sets cache headers of latest matching page" do
it "sets etag" do
get alchemy_json_api.pages_path(filter: {page_layout_eq: "news"})
expect(response.headers["Last-Modified"]).to eq(news_page2.published_at.utc.httpdate)
expect(response.headers["ETag"]).to eq('W/"e7a1c8beb22b58e94a605594d79766ad"')
expect(response.headers["Last-Modified"]).to be_nil
expect(response.headers["ETag"]).to eq('W/"f6dd8e0c4668326e349ff0c947d9f42b"')
expect(response.headers["Cache-Control"]).to eq("max-age=10800, public, must-revalidate")
end
end
Expand Down

0 comments on commit 9343574

Please sign in to comment.