From 3a90d776a3474d8cba079752347e12a90d2d9fb0 Mon Sep 17 00:00:00 2001 From: Christian Schmidt Date: Fri, 4 Oct 2024 23:03:11 +0200 Subject: [PATCH 1/4] New feature: Acceptable --- lib/http/feature.rb | 1 + lib/http/features/acceptable.rb | 43 ++++++++ spec/lib/http/features/acceptable_spec.rb | 116 ++++++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 lib/http/features/acceptable.rb create mode 100644 spec/lib/http/features/acceptable_spec.rb diff --git a/lib/http/feature.rb b/lib/http/feature.rb index b178becf..3c855de9 100644 --- a/lib/http/feature.rb +++ b/lib/http/feature.rb @@ -16,6 +16,7 @@ def on_error(_request, _error); end require "http/features/auto_inflate" require "http/features/auto_deflate" +require "http/features/acceptable" require "http/features/logging" require "http/features/instrumentation" require "http/features/normalize_uri" diff --git a/lib/http/features/acceptable.rb b/lib/http/features/acceptable.rb new file mode 100644 index 00000000..2edf7172 --- /dev/null +++ b/lib/http/features/acceptable.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module HTTP + module Features + class Acceptable < Feature + def wrap_response(response) + return response if accepted?(response) + + Response.new( + status: 406, + version: response.version, + headers: response.headers, + proxy_headers: response.proxy_headers, + connection: response.connection, + body: response.body, + request: response.request + ) + end + + private + + def accepted?(response) + accept = response.request[Headers::ACCEPT] + + return true unless accept + + ranges = accept.split(/\s*,\s*/).map { |r| r.gsub(/\s*;.*/, "") } + ranges.any? { |range| match?(response.mime_type, range) } + end + + def match?(mime_type, range) + return true if range == "*/*" + + m1, m2 = mime_type.split("/", 2) + r1, r2 = range.split("/", 2) + + m1 == r1 && ["*", m2].include?(r2) + end + + HTTP::Options.register_feature(:acceptable, self) + end + end +end diff --git a/spec/lib/http/features/acceptable_spec.rb b/spec/lib/http/features/acceptable_spec.rb new file mode 100644 index 00000000..7f3082bf --- /dev/null +++ b/spec/lib/http/features/acceptable_spec.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +RSpec.describe HTTP::Features::Acceptable do + subject(:feature) { described_class.new } + + let(:connection) { double } + let(:headers) { {} } + + describe "#accepted?" do + subject(:result) { feature.wrap_response(response) } + + let(:request) do + HTTP::Request.new( + verb: :get, + uri: "https://example.com/", + headers: headers, + body: "Hello world" + ) + end + let(:response) do + HTTP::Response.new( + version: "1.1", + status: 200, + headers: { "content-type": "text/html; charset=utf-8" }, + connection: connection, + request: request + ) + end + + context "when there is no Accept header" do + it "returns original request" do + expect(result).to be response + end + end + + context "when MIME type matches single range" do + let(:headers) { { accept: "text/html" } } + + it "returns original request" do + expect(result).to be response + end + end + + context "when MIME type matches range with parameter" do + let(:headers) { { accept: "text/html; q=1" } } + + it "returns original request" do + expect(result).to be response + end + end + + context "when MIME type matches one of multiple ranges" do + let(:headers) { { accept: "text/plain, text/html, image/gif" } } + + it "returns original request" do + expect(result).to be response + end + end + + context "when type matches and subtype does not" do + let(:headers) { { accept: "text/plain" } } + + it "returns synthetic 406 status" do + expect(result.code).to be 406 + end + + it "returns original version" do + expect(result.version).to be response.version + end + + it "returns original headers" do + expect(result.headers).to eq response.headers + end + + it "returns original connection" do + expect(result.connection).to be response.connection + end + + it "returns original request" do + expect(result.request).to be request + end + end + + context "when both type and subtype do not match" do + let(:headers) { { accept: "image/gif" } } + + it "returns original request" do + expect(result.code).to be 406 + end + end + + context "when range is */*" do + let(:headers) { { accept: "*/*" } } + + it "returns original request" do + expect(result).to be response + end + end + + context "when type matches and subtype is wildcard" do + let(:headers) { { accept: "text/*" } } + + it "returns original request" do + expect(result).to be response + end + end + + context "when type does not match and subtype is wildcard" do + let(:headers) { { accept: "image/*" } } + + it "returns original request" do + expect(result.code).to be 406 + end + end + end +end From c9e8f5d0e79d35b8d632cd6906ca967635723e2c Mon Sep 17 00:00:00 2001 From: Christian Schmidt Date: Fri, 4 Oct 2024 23:11:47 +0200 Subject: [PATCH 2/4] Mention in comment --- lib/http/chainable.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/http/chainable.rb b/lib/http/chainable.rb index 188f6c37..b3ba0438 100644 --- a/lib/http/chainable.rb +++ b/lib/http/chainable.rb @@ -238,6 +238,7 @@ def nodelay end # Turn on given features. Available features are: + # * acceptable # * auto_inflate # * auto_deflate # * instrumentation From 2afb06137dbbd6ea17b64de8b9e8b6131b1e505e Mon Sep 17 00:00:00 2001 From: Christian Schmidt Date: Fri, 4 Oct 2024 23:26:24 +0200 Subject: [PATCH 3/4] Clean up --- spec/lib/http/features/acceptable_spec.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spec/lib/http/features/acceptable_spec.rb b/spec/lib/http/features/acceptable_spec.rb index 7f3082bf..a5f8a39f 100644 --- a/spec/lib/http/features/acceptable_spec.rb +++ b/spec/lib/http/features/acceptable_spec.rb @@ -6,7 +6,7 @@ let(:connection) { double } let(:headers) { {} } - describe "#accepted?" do + describe "#wrap_response" do subject(:result) { feature.wrap_response(response) } let(:request) do @@ -14,7 +14,6 @@ verb: :get, uri: "https://example.com/", headers: headers, - body: "Hello world" ) end let(:response) do From ce6a0d7cbf9aa994b204afa888df3c8826729497 Mon Sep 17 00:00:00 2001 From: Christian Schmidt Date: Fri, 4 Oct 2024 23:28:38 +0200 Subject: [PATCH 4/4] Lint --- spec/lib/http/features/acceptable_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/http/features/acceptable_spec.rb b/spec/lib/http/features/acceptable_spec.rb index a5f8a39f..f3230f9f 100644 --- a/spec/lib/http/features/acceptable_spec.rb +++ b/spec/lib/http/features/acceptable_spec.rb @@ -13,7 +13,7 @@ HTTP::Request.new( verb: :get, uri: "https://example.com/", - headers: headers, + headers: headers ) end let(:response) do