diff --git a/lib/httpi.rb b/lib/httpi.rb index 61613c2..f4c377b 100644 --- a/lib/httpi.rb +++ b/lib/httpi.rb @@ -3,6 +3,7 @@ require "httpi/logger" require "httpi/request" require "httpi/query_builder" +require "httpi/utils" require "httpi/adapter/httpclient" require "httpi/adapter/curb" diff --git a/lib/httpi/request.rb b/lib/httpi/request.rb index 786e14c..b646795 100644 --- a/lib/httpi/request.rb +++ b/lib/httpi/request.rb @@ -64,12 +64,12 @@ def ssl? # Returns a Hash of HTTP headers. Defaults to return an empty Hash. def headers - @headers ||= Rack::Headers.new + @headers ||= HTTPI::Utils::Headers.new end # Sets the Hash of HTTP headers. def headers=(headers) - @headers = Rack::Headers.new.merge(headers) + @headers = HTTPI::Utils::Headers.new.merge(headers) end # Adds a header information to accept gzipped content. diff --git a/lib/httpi/response.rb b/lib/httpi/response.rb index f9b7c4f..ecc3d41 100644 --- a/lib/httpi/response.rb +++ b/lib/httpi/response.rb @@ -20,7 +20,7 @@ class Response # Initializer expects an HTTP response +code+, +headers+ and +body+. def initialize(code, headers, body) self.code = code.to_i - self.headers = Rack::Headers.new.merge(headers) + self.headers = HTTPI::Utils::Headers.new.merge(headers) self.raw_body = body end diff --git a/lib/httpi/utils.rb b/lib/httpi/utils.rb new file mode 100644 index 0000000..ddc16e6 --- /dev/null +++ b/lib/httpi/utils.rb @@ -0,0 +1,238 @@ +# mostly verbatim from: https://github.com/rack/rack/blob/main/lib/rack/headers.rb +# Because this is part of httpi's public API, its better not to load an external +# library for it. +module HTTPI + module Utils + # HTTPI::Utils::Headers is a Hash subclass that downcases all keys. + class Headers < Hash + KNOWN_HEADERS = {} + %w( + Accept-CH + Accept-Patch + Accept-Ranges + Access-Control-Allow-Credentials + Access-Control-Allow-Headers + Access-Control-Allow-Methods + Access-Control-Allow-Origin + Access-Control-Expose-Headers + Access-Control-Max-Age + Age + Allow + Alt-Svc + Cache-Control + Connection + Content-Disposition + Content-Encoding + Content-Language + Content-Length + Content-Location + Content-MD5 + Content-Range + Content-Security-Policy + Content-Security-Policy-Report-Only + Content-Type + Date + Delta-Base + ETag + Expect-CT + Expires + Feature-Policy + IM + Last-Modified + Link + Location + NEL + P3P + Permissions-Policy + Pragma + Preference-Applied + Proxy-Authenticate + Public-Key-Pins + Referrer-Policy + Refresh + Report-To + Retry-After + Server + Set-Cookie + Status + Strict-Transport-Security + Timing-Allow-Origin + Tk + Trailer + Transfer-Encoding + Upgrade + Vary + Via + WWW-Authenticate + Warning + X-Cascade + X-Content-Duration + X-Content-Security-Policy + X-Content-Type-Options + X-Correlation-ID + X-Correlation-Id + X-Download-Options + X-Frame-Options + X-Permitted-Cross-Domain-Policies + X-Powered-By + X-Redirect-By + X-Request-ID + X-Request-Id + X-Runtime + X-UA-Compatible + X-WebKit-CS + X-XSS-Protection + ).each do |str| + downcased = str.downcase.freeze + KNOWN_HEADERS[str] = KNOWN_HEADERS[downcased] = downcased + end + + def self.[](*items) + if items.length % 2 != 0 + if items.length == 1 && items.first.is_a?(Hash) + new.merge!(items.first) + else + raise ArgumentError, "odd number of arguments for Utils::Headers" + end + else + hash = new + loop do + break if items.length == 0 + key = items.shift + value = items.shift + hash[key] = value + end + hash + end + end + + def [](key) + super(downcase_key(key)) + end + + def []=(key, value) + super(KNOWN_HEADERS[key] || key.downcase.freeze, value) + end + alias store []= + + def assoc(key) + super(downcase_key(key)) + end + + def compare_by_identity + raise TypeError, "Utils::Headers cannot compare by identity, use regular Hash" + end + + def delete(key) + super(downcase_key(key)) + end + + def dig(key, *a) + super(downcase_key(key), *a) + end + + def fetch(key, *default, &block) + key = downcase_key(key) + super + end + + def fetch_values(*a) + super(*a.map!{|key| downcase_key(key)}) + end + + def has_key?(key) + super(downcase_key(key)) + end + alias include? has_key? + alias key? has_key? + alias member? has_key? + + def invert + hash = self.class.new + each{|key, value| hash[value] = key} + hash + end + + def merge(hash, &block) + dup.merge!(hash, &block) + end + + def reject(&block) + hash = dup + hash.reject!(&block) + hash + end + + def replace(hash) + clear + update(hash) + end + + def select(&block) + hash = dup + hash.select!(&block) + hash + end + + def to_proc + lambda{|x| self[x]} + end + + def transform_values(&block) + dup.transform_values!(&block) + end + + def update(hash, &block) + hash.each do |key, value| + self[key] = if block_given? && include?(key) + block.call(key, self[key], value) + else + value + end + end + self + end + alias merge! update + + def values_at(*keys) + keys.map{|key| self[key]} + end + + # :nocov: + if RUBY_VERSION >= '2.5' + # :nocov: + def slice(*a) + h = self.class.new + a.each{|k| h[k] = self[k] if has_key?(k)} + h + end + + def transform_keys(&block) + dup.transform_keys!(&block) + end + + def transform_keys! + hash = self.class.new + each do |k, v| + hash[yield k] = v + end + replace(hash) + end + end + + # :nocov: + if RUBY_VERSION >= '3.0' + # :nocov: + def except(*a) + super(*a.map!{|key| downcase_key(key)}) + end + end + + private + + def downcase_key(key) + key.is_a?(String) ? KNOWN_HEADERS[key] || key.downcase : key + end + end + end +end diff --git a/spec/httpi/request_spec.rb b/spec/httpi/request_spec.rb index efd88ae..3d30528 100644 --- a/spec/httpi/request_spec.rb +++ b/spec/httpi/request_spec.rb @@ -149,7 +149,7 @@ describe "#headers" do it "lets you specify a Hash of HTTP request headers" do request.headers = { "Accept-Encoding" => "gzip" } - expect(request.headers).to eq Rack::Headers.new.merge({ "Accept-Encoding" => "gzip" }) + expect(request.headers).to eq HTTPI::Utils::Headers.new.merge({ "Accept-Encoding" => "gzip" }) end it "defaults to return an empty Hash" do diff --git a/spec/httpi/response_spec.rb b/spec/httpi/response_spec.rb index d797bb3..699905e 100644 --- a/spec/httpi/response_spec.rb +++ b/spec/httpi/response_spec.rb @@ -88,7 +88,7 @@ describe "#headers" do it "returns the HTTP response headers" do - expect(response.headers).to eq Rack::Headers.new.merge({ "Content-Encoding" => "gzip" }) + expect(response.headers).to eq HTTPI::Utils::Headers.new.merge({ "Content-Encoding" => "gzip" }) end end @@ -116,7 +116,7 @@ describe "#headers" do it "returns the HTTP response headers" do - expect(response.headers).to eq Rack::Headers.new.merge({ "Content-Type" => "application/dime" }) + expect(response.headers).to eq HTTPI::Utils::Headers.new.merge({ "Content-Type" => "application/dime" }) end end