FIX: Cache failed onebox URL request server-side (#8421)

We already cache failed onebox URL requests client-side, we now want to cache this on the server-side for extra protection. failed onebox previews will be cached for 1 hour, and any more requests for that URL will fail with a 404 status. Forcing a rebake via the Rebake HTML action will delete the failed URL cache (like how the oneboxer preview cache is deleted).
This commit is contained in:
Martin Brennan 2019-11-28 07:48:29 +10:00 committed by GitHub
parent e7c7a05097
commit 901054fd75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 50 additions and 4 deletions

View File

@ -19,6 +19,8 @@ class OneboxController < ApplicationController
invalidate = params[:refresh] == 'true' invalidate = params[:refresh] == 'true'
url = params[:url] url = params[:url]
return render(body: nil, status: 404) if Oneboxer.recently_failed?(url)
hijack do hijack do
Oneboxer.preview_onebox!(user_id) Oneboxer.preview_onebox!(user_id)
@ -34,6 +36,7 @@ class OneboxController < ApplicationController
Oneboxer.onebox_previewed!(user_id) Oneboxer.onebox_previewed!(user_id)
if preview.blank? if preview.blank?
Oneboxer.cache_failed!(url)
render body: nil, status: 404 render body: nil, status: 404
else else
render plain: preview render plain: preview

View File

@ -72,6 +72,7 @@ module Oneboxer
def self.invalidate(url) def self.invalidate(url)
Discourse.cache.delete(onebox_cache_key(url)) Discourse.cache.delete(onebox_cache_key(url))
Discourse.cache.delete(onebox_failed_cache_key(url))
end end
# Parse URLs out of HTML, returning the document when finished. # Parse URLs out of HTML, returning the document when finished.
@ -136,6 +137,14 @@ module Oneboxer
Onebox::Matcher.new(url).oneboxed Onebox::Matcher.new(url).oneboxed
end end
def self.recently_failed?(url)
Discourse.cache.read(onebox_failed_cache_key(url)).present?
end
def self.cache_failed!(url)
Discourse.cache.write(onebox_failed_cache_key(url), true, expires_in: 1.hour)
end
private private
def self.preview_key(user_id) def self.preview_key(user_id)
@ -150,6 +159,10 @@ module Oneboxer
"onebox__#{url}" "onebox__#{url}"
end end
def self.onebox_failed_cache_key(url)
"onebox_failed__#{url}"
end
def self.onebox_raw(url, opts = {}) def self.onebox_raw(url, opts = {})
url = URI(url).to_s url = URI(url).to_s
local_onebox(url, opts) || external_onebox(url) local_onebox(url, opts) || external_onebox(url)

View File

@ -12,6 +12,17 @@ describe Oneboxer do
expect(Oneboxer.onebox("http://boom.com")).to eq("") expect(Oneboxer.onebox("http://boom.com")).to eq("")
end end
describe "#invalidate" do
let(:url) { "http://test.com" }
it "clears the cached preview for the onebox URL and the failed URL cache" do
Discourse.cache.write(Oneboxer.onebox_cache_key(url), "test")
Discourse.cache.write(Oneboxer.onebox_failed_cache_key(url), true)
Oneboxer.invalidate(url)
expect(Discourse.cache.read(Oneboxer.onebox_cache_key(url))).to eq(nil)
expect(Discourse.cache.read(Oneboxer.onebox_failed_cache_key(url))).to eq(nil)
end
end
context "local oneboxes" do context "local oneboxes" do
def link(url) def link(url)

View File

@ -4,6 +4,10 @@ require 'rails_helper'
describe OneboxController do describe OneboxController do
before do
Discourse.cache.delete(Oneboxer.onebox_failed_cache_key(url))
end
let(:url) { "http://google.com" } let(:url) { "http://google.com" }
it "requires the user to be logged in" do it "requires the user to be logged in" do
@ -112,19 +116,34 @@ describe OneboxController do
end end
describe "missing onebox" do describe "missing onebox" do
it "returns 404 if the onebox is nil" do def stub_request_to_onebox_url(response_body)
stub_request(:head, url) stub_request(:head, url)
stub_request(:get, url).to_return(body: nil).then.to_raise stub_request(:get, url).to_return(body: response_body).then.to_raise
end
it "returns 404 if the onebox is nil" do
stub_request_to_onebox_url(nil)
get "/onebox.json", params: { url: url, refresh: "true" } get "/onebox.json", params: { url: url, refresh: "true" }
expect(response.response_code).to eq(404) expect(response.response_code).to eq(404)
end end
it "returns 404 if the onebox is an empty string" do it "returns 404 if the onebox is an empty string" do
stub_request(:head, url) stub_request_to_onebox_url(" \t ")
stub_request(:get, url).to_return(body: " \t ").then.to_raise
get "/onebox.json", params: { url: url, refresh: "true" } get "/onebox.json", params: { url: url, refresh: "true" }
expect(response.response_code).to eq(404) expect(response.response_code).to eq(404)
end end
it "cases missing onebox URLs so we do not attempt to preview again" do
stub_request_to_onebox_url(nil)
get "/onebox.json", params: { url: url, refresh: "true" }
expect(response.response_code).to eq(404)
Oneboxer.expects(:preview_onebox!).never
get "/onebox.json", params: { url: url, refresh: "true" }
expect(response.response_code).to eq(404)
expect(
Discourse.cache.read(Oneboxer.onebox_failed_cache_key(url))
).not_to eq(nil)
end
end end
describe "local onebox" do describe "local onebox" do