http 0.8.14 → 0.9.0.pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGES.md +3 -7
- data/Gemfile +3 -6
- data/README.md +12 -22
- data/lib/http/chainable.rb +0 -8
- data/lib/http/client.rb +9 -11
- data/lib/http/errors.rb +0 -3
- data/lib/http/options.rb +12 -27
- data/lib/http/request.rb +2 -21
- data/lib/http/response.rb +0 -6
- data/lib/http/timeout/null.rb +24 -1
- data/lib/http/timeout/per_operation.rb +8 -24
- data/lib/http/uri.rb +2 -7
- data/lib/http/version.rb +1 -1
- data/spec/lib/http/client_spec.rb +1 -31
- data/spec/lib/http/options/merge_spec.rb +0 -1
- data/spec/lib/http/request_spec.rb +2 -7
- data/spec/lib/http/response_spec.rb +0 -5
- data/spec/lib/http_spec.rb +0 -8
- data/spec/spec_helper.rb +5 -0
- data/spec/support/connection_reuse_shared.rb +2 -0
- data/spec/support/http_handling_shared.rb +7 -0
- metadata +36 -19
- data/lib/http/cache.rb +0 -146
- data/lib/http/cache/headers.rb +0 -100
- data/lib/http/cache/null_cache.rb +0 -13
- data/lib/http/request/caching.rb +0 -95
- data/lib/http/response/caching.rb +0 -143
- data/lib/http/response/io_body.rb +0 -63
- data/lib/http/response/string_body.rb +0 -53
- data/spec/lib/http/cache/headers_spec.rb +0 -77
- data/spec/lib/http/cache_spec.rb +0 -182
- data/spec/lib/http/request/caching_spec.rb +0 -133
- data/spec/lib/http/response/caching_spec.rb +0 -201
- data/spec/lib/http/response/io_body_spec.rb +0 -35
- data/spec/lib/http/response/string_body_spec.rb +0 -35
@@ -1,13 +0,0 @@
|
|
1
|
-
module HTTP
|
2
|
-
class Cache
|
3
|
-
# NoOp cache. Always makes the request. Allows avoiding
|
4
|
-
# conditionals in the request flow.
|
5
|
-
class NullCache
|
6
|
-
# @return [Response] the result of the provided block
|
7
|
-
# @yield [request, options] so that the request can actually be made
|
8
|
-
def perform(request, options)
|
9
|
-
yield(request, options)
|
10
|
-
end
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
data/lib/http/request/caching.rb
DELETED
@@ -1,95 +0,0 @@
|
|
1
|
-
require "http/headers"
|
2
|
-
require "http/cache/headers"
|
3
|
-
|
4
|
-
module HTTP
|
5
|
-
class Request
|
6
|
-
# Decorator for requests to provide convenience methods related to caching.
|
7
|
-
class Caching < DelegateClass(HTTP::Request)
|
8
|
-
INVALIDATING_METHODS = [:post, :put, :delete, :patch].freeze
|
9
|
-
CACHEABLE_METHODS = [:get, :head].freeze
|
10
|
-
|
11
|
-
# When was this request sent to the server
|
12
|
-
#
|
13
|
-
# @api public
|
14
|
-
attr_accessor :sent_at
|
15
|
-
|
16
|
-
# Inits a new instance
|
17
|
-
# @api private
|
18
|
-
def initialize(obj)
|
19
|
-
super
|
20
|
-
@requested_at = nil
|
21
|
-
@received_at = nil
|
22
|
-
end
|
23
|
-
|
24
|
-
# @return [HTTP::Request::Caching]
|
25
|
-
def caching
|
26
|
-
self
|
27
|
-
end
|
28
|
-
|
29
|
-
# @return [Boolean] true iff request demands the resources cache entry be invalidated
|
30
|
-
#
|
31
|
-
# @api public
|
32
|
-
def invalidates_cache?
|
33
|
-
INVALIDATING_METHODS.include?(verb) ||
|
34
|
-
cache_headers.no_store?
|
35
|
-
end
|
36
|
-
|
37
|
-
# @return [Boolean] true if request is cacheable
|
38
|
-
#
|
39
|
-
# @api public
|
40
|
-
def cacheable?
|
41
|
-
CACHEABLE_METHODS.include?(verb) &&
|
42
|
-
!cache_headers.no_store?
|
43
|
-
end
|
44
|
-
|
45
|
-
# @return [Boolean] true iff the cache control info of this
|
46
|
-
# request demands that the response be revalidated by the origin
|
47
|
-
# server.
|
48
|
-
#
|
49
|
-
# @api public
|
50
|
-
def skips_cache?
|
51
|
-
0 == cache_headers.max_age ||
|
52
|
-
cache_headers.must_revalidate? ||
|
53
|
-
cache_headers.no_cache?
|
54
|
-
end
|
55
|
-
|
56
|
-
# @return [HTTP::Request::Caching] new request based on this
|
57
|
-
# one but conditional on the resource having changed since
|
58
|
-
# `cached_response`
|
59
|
-
#
|
60
|
-
# @api public
|
61
|
-
def conditional_on_changes_to(cached_response)
|
62
|
-
self.class.new HTTP::Request.new(
|
63
|
-
verb, uri, headers.merge(conditional_headers_for(cached_response)),
|
64
|
-
proxy, body, version).caching
|
65
|
-
end
|
66
|
-
|
67
|
-
# @return [HTTP::Cache::Headers] cache control helper for this request
|
68
|
-
# @api public
|
69
|
-
def cache_headers
|
70
|
-
@cache_headers ||= HTTP::Cache::Headers.new headers
|
71
|
-
end
|
72
|
-
|
73
|
-
def env
|
74
|
-
{"rack-cache.cache_key" => lambda { |r| r.uri.to_s }}
|
75
|
-
end
|
76
|
-
|
77
|
-
private
|
78
|
-
|
79
|
-
# @return [Headers] conditional request headers
|
80
|
-
def conditional_headers_for(cached_response)
|
81
|
-
headers = HTTP::Headers.new
|
82
|
-
|
83
|
-
cached_response.headers.get(HTTP::Headers::ETAG).
|
84
|
-
each { |etag| headers.add(HTTP::Headers::IF_NONE_MATCH, etag) }
|
85
|
-
|
86
|
-
cached_response.headers.get(HTTP::Headers::LAST_MODIFIED).
|
87
|
-
each { |last_mod| headers.add(HTTP::Headers::IF_MODIFIED_SINCE, last_mod) }
|
88
|
-
|
89
|
-
headers.add(HTTP::Headers::CACHE_CONTROL, "max-age=0") if cache_headers.forces_revalidation?
|
90
|
-
|
91
|
-
headers
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|
95
|
-
end
|
@@ -1,143 +0,0 @@
|
|
1
|
-
require "http/headers"
|
2
|
-
require "http/cache/headers"
|
3
|
-
require "http/response/string_body"
|
4
|
-
require "http/response/io_body"
|
5
|
-
|
6
|
-
module HTTP
|
7
|
-
class Response
|
8
|
-
# Decorator class for responses to provide convenience methods
|
9
|
-
# related to caching.
|
10
|
-
class Caching < DelegateClass(HTTP::Response)
|
11
|
-
CACHEABLE_RESPONSE_CODES = [200, 203, 300, 301, 410].freeze
|
12
|
-
|
13
|
-
def initialize(obj)
|
14
|
-
super
|
15
|
-
@requested_at = nil
|
16
|
-
@received_at = nil
|
17
|
-
end
|
18
|
-
|
19
|
-
# @return [HTTP::Response::Caching]
|
20
|
-
def caching
|
21
|
-
self
|
22
|
-
end
|
23
|
-
|
24
|
-
# @return [Boolean] true iff this response is stale
|
25
|
-
def stale?
|
26
|
-
expired? || cache_headers.must_revalidate?
|
27
|
-
end
|
28
|
-
|
29
|
-
# @return [Boolean] true iff this response has expired
|
30
|
-
def expired?
|
31
|
-
current_age >= cache_headers.max_age
|
32
|
-
end
|
33
|
-
|
34
|
-
# @return [Boolean] true iff this response is cacheable
|
35
|
-
#
|
36
|
-
# ---
|
37
|
-
# A Vary header field-value of "*" always fails to match and
|
38
|
-
# subsequent requests on that resource can only be properly
|
39
|
-
# interpreted by the
|
40
|
-
def cacheable?
|
41
|
-
@cacheable ||=
|
42
|
-
begin
|
43
|
-
CACHEABLE_RESPONSE_CODES.include?(code) \
|
44
|
-
&& !(cache_headers.vary_star? ||
|
45
|
-
cache_headers.no_store? ||
|
46
|
-
cache_headers.no_cache?)
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
# @return [Numeric] the current age (in seconds) of this response
|
51
|
-
#
|
52
|
-
# ---
|
53
|
-
# Algo from https://tools.ietf.org/html/rfc2616#section-13.2.3
|
54
|
-
def current_age
|
55
|
-
now = Time.now
|
56
|
-
age_value = headers.get(HTTP::Headers::AGE).map(&:to_i).max || 0
|
57
|
-
|
58
|
-
apparent_age = [0, received_at - server_response_time].max
|
59
|
-
corrected_received_age = [apparent_age, age_value].max
|
60
|
-
response_delay = [0, received_at - requested_at].max
|
61
|
-
corrected_initial_age = corrected_received_age + response_delay
|
62
|
-
resident_time = [0, now - received_at].max
|
63
|
-
|
64
|
-
corrected_initial_age + resident_time
|
65
|
-
end
|
66
|
-
|
67
|
-
# @return [Time] the time at which this response was requested
|
68
|
-
def requested_at
|
69
|
-
@requested_at ||= received_at
|
70
|
-
end
|
71
|
-
attr_writer :requested_at
|
72
|
-
|
73
|
-
# @return [Time] the time at which this response was received
|
74
|
-
def received_at
|
75
|
-
@received_at ||= Time.now
|
76
|
-
end
|
77
|
-
attr_writer :received_at
|
78
|
-
|
79
|
-
# Update self based on this response being revalidated by the
|
80
|
-
# server.
|
81
|
-
def validated!(validating_response)
|
82
|
-
headers.merge!(validating_response.headers)
|
83
|
-
self.requested_at = validating_response.requested_at
|
84
|
-
self.received_at = validating_response.received_at
|
85
|
-
end
|
86
|
-
|
87
|
-
# @return [HTTP::Cache::Headers] cache control headers helper object.
|
88
|
-
def cache_headers
|
89
|
-
@cache_headers ||= HTTP::Cache::Headers.new headers
|
90
|
-
end
|
91
|
-
|
92
|
-
def body
|
93
|
-
@body ||= if __getobj__.body.respond_to? :each
|
94
|
-
__getobj__.body
|
95
|
-
else
|
96
|
-
StringBody.new(__getobj__.body.to_s)
|
97
|
-
end
|
98
|
-
end
|
99
|
-
|
100
|
-
def body=(new_body)
|
101
|
-
@body = if new_body.respond_to?(:readpartial) && new_body.respond_to?(:read)
|
102
|
-
# IO-ish, probably a rack cache response body
|
103
|
-
IoBody.new(new_body)
|
104
|
-
|
105
|
-
elsif new_body.respond_to? :join
|
106
|
-
# probably an array of body parts (rack cache does this sometimes)
|
107
|
-
StringBody.new(new_body.join(""))
|
108
|
-
|
109
|
-
elsif new_body.respond_to? :readpartial
|
110
|
-
# normal body, just use it.
|
111
|
-
new_body
|
112
|
-
|
113
|
-
else
|
114
|
-
# backstop, just to_s it
|
115
|
-
StringBody.new(new_body.to_s)
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
def vary
|
120
|
-
headers.get(HTTP::Headers::VARY).first
|
121
|
-
end
|
122
|
-
|
123
|
-
protected
|
124
|
-
|
125
|
-
# @return [Time] the time at which the server generated this response.
|
126
|
-
def server_response_time
|
127
|
-
headers.get(HTTP::Headers::DATE).
|
128
|
-
map(&method(:to_time_or_epoch)).
|
129
|
-
max || begin
|
130
|
-
# set it if it is not already set
|
131
|
-
headers[HTTP::Headers::DATE] = received_at.httpdate
|
132
|
-
received_at
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
def to_time_or_epoch(t_str)
|
137
|
-
Time.httpdate(t_str)
|
138
|
-
rescue ArgumentError
|
139
|
-
Time.at(0)
|
140
|
-
end
|
141
|
-
end
|
142
|
-
end
|
143
|
-
end
|
@@ -1,63 +0,0 @@
|
|
1
|
-
module HTTP
|
2
|
-
class Response
|
3
|
-
# A Body class that wraps an IO, rather than a the client
|
4
|
-
# object.
|
5
|
-
class IoBody
|
6
|
-
include Enumerable
|
7
|
-
extend Forwardable
|
8
|
-
|
9
|
-
# @return [String,nil] the next `size` octets part of the
|
10
|
-
# body, or nil if whole body has already been read.
|
11
|
-
def readpartial(size = HTTP::Connection::BUFFER_SIZE)
|
12
|
-
stream!
|
13
|
-
return nil if stream.eof?
|
14
|
-
|
15
|
-
stream.readpartial(size)
|
16
|
-
end
|
17
|
-
|
18
|
-
# Iterate over the body, allowing it to be enumerable
|
19
|
-
def each
|
20
|
-
while (part = readpartial)
|
21
|
-
yield part
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
# @return [String] eagerly consume the entire body as a string
|
26
|
-
def to_s
|
27
|
-
@contents ||= readall
|
28
|
-
end
|
29
|
-
alias_method :to_str, :to_s
|
30
|
-
|
31
|
-
def_delegator :to_s, :empty?
|
32
|
-
|
33
|
-
# Assert that the body is actively being streamed
|
34
|
-
def stream!
|
35
|
-
fail StateError, "body has already been consumed" if @streaming == false
|
36
|
-
@streaming = true
|
37
|
-
end
|
38
|
-
|
39
|
-
# Easier to interpret string inspect
|
40
|
-
def inspect
|
41
|
-
"#<#{self.class}:#{object_id.to_s(16)} @streaming=#{!!@streaming}>"
|
42
|
-
end
|
43
|
-
|
44
|
-
protected
|
45
|
-
|
46
|
-
def initialize(an_io)
|
47
|
-
@streaming = nil
|
48
|
-
@stream = an_io
|
49
|
-
end
|
50
|
-
|
51
|
-
attr_reader :contents, :stream
|
52
|
-
|
53
|
-
def readall
|
54
|
-
fail StateError, "body is being streamed" unless @streaming.nil?
|
55
|
-
|
56
|
-
@streaming = false
|
57
|
-
"".tap do |buf|
|
58
|
-
buf << stream.read until stream.eof?
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
@@ -1,53 +0,0 @@
|
|
1
|
-
module HTTP
|
2
|
-
class Response
|
3
|
-
# A Body class that wraps a String, rather than a the client
|
4
|
-
# object.
|
5
|
-
class StringBody
|
6
|
-
include Enumerable
|
7
|
-
extend Forwardable
|
8
|
-
|
9
|
-
# @return [String,nil] the next `size` octets part of the
|
10
|
-
# body, or nil if whole body has already been read.
|
11
|
-
def readpartial(size = @contents.length)
|
12
|
-
stream!
|
13
|
-
return nil if @streaming_offset >= @contents.length
|
14
|
-
|
15
|
-
@contents[@streaming_offset, size].tap do |part|
|
16
|
-
@streaming_offset += (part.length + 1)
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
# Iterate over the body, allowing it to be enumerable
|
21
|
-
def each
|
22
|
-
yield @contents
|
23
|
-
end
|
24
|
-
|
25
|
-
# @return [String] eagerly consume the entire body as a string
|
26
|
-
def to_s
|
27
|
-
@contents
|
28
|
-
end
|
29
|
-
alias_method :to_str, :to_s
|
30
|
-
|
31
|
-
def_delegator :@contents, :empty?
|
32
|
-
|
33
|
-
# Assert that the body is actively being streamed
|
34
|
-
def stream!
|
35
|
-
fail StateError, "body has already been consumed" if @streaming == false
|
36
|
-
@streaming = true
|
37
|
-
end
|
38
|
-
|
39
|
-
# Easier to interpret string inspect
|
40
|
-
def inspect
|
41
|
-
"#<#{self.class}:#{object_id.to_s(16)}>"
|
42
|
-
end
|
43
|
-
|
44
|
-
protected
|
45
|
-
|
46
|
-
def initialize(contents)
|
47
|
-
@contents = contents
|
48
|
-
@streaming = nil
|
49
|
-
@streaming_offset = 0
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
@@ -1,77 +0,0 @@
|
|
1
|
-
RSpec.describe HTTP::Cache::Headers do
|
2
|
-
subject(:cache_headers) { described_class.new headers }
|
3
|
-
|
4
|
-
describe ".new" do
|
5
|
-
it "accepts instance of HTTP::Headers" do
|
6
|
-
expect { described_class.new HTTP::Headers.new }.not_to raise_error
|
7
|
-
end
|
8
|
-
|
9
|
-
it "it rejects any object that does not respond to #headers" do
|
10
|
-
expect { described_class.new double }.to raise_error HTTP::Error
|
11
|
-
end
|
12
|
-
end
|
13
|
-
|
14
|
-
context "with <Cache-Control: private>" do
|
15
|
-
let(:headers) { {"Cache-Control" => "private"} }
|
16
|
-
it { is_expected.to be_private }
|
17
|
-
end
|
18
|
-
|
19
|
-
context "with <Cache-Control: public>" do
|
20
|
-
let(:headers) { {"Cache-Control" => "public"} }
|
21
|
-
it { is_expected.to be_public }
|
22
|
-
end
|
23
|
-
|
24
|
-
context "with <Cache-Control: no-cache>" do
|
25
|
-
let(:headers) { {"Cache-Control" => "no-cache"} }
|
26
|
-
it { is_expected.to be_no_cache }
|
27
|
-
end
|
28
|
-
|
29
|
-
context "with <Cache-Control: no-store>" do
|
30
|
-
let(:headers) { {"Cache-Control" => "no-store"} }
|
31
|
-
it { is_expected.to be_no_store }
|
32
|
-
end
|
33
|
-
|
34
|
-
describe "#max_age" do
|
35
|
-
subject { cache_headers.max_age }
|
36
|
-
|
37
|
-
context "with <Cache-Control: max-age=100>" do
|
38
|
-
let(:headers) { {"Cache-Control" => "max-age=100"} }
|
39
|
-
it { is_expected.to eq 100 }
|
40
|
-
end
|
41
|
-
|
42
|
-
context "with <Expires: {100 seconds from now}>" do
|
43
|
-
let(:headers) { {"Expires" => (Time.now + 100).httpdate} }
|
44
|
-
it { is_expected.to be_within(1).of(100) }
|
45
|
-
end
|
46
|
-
|
47
|
-
context "with <Expires: {100 seconds before now}>" do
|
48
|
-
let(:headers) { {"Expires" => (Time.now - 100).httpdate} }
|
49
|
-
it { is_expected.to eq 0 }
|
50
|
-
end
|
51
|
-
|
52
|
-
context "with <Expires: -1>" do
|
53
|
-
let(:headers) { {"Expires" => "-1"} }
|
54
|
-
it { is_expected.to eq 0 }
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
context "with <Vary: *>" do
|
59
|
-
let(:headers) { {"Vary" => "*"} }
|
60
|
-
it { is_expected.to be_vary_star }
|
61
|
-
end
|
62
|
-
|
63
|
-
context "with no cache related headers" do
|
64
|
-
let(:headers) { {} }
|
65
|
-
|
66
|
-
it { is_expected.not_to be_private }
|
67
|
-
it { is_expected.not_to be_public }
|
68
|
-
it { is_expected.not_to be_no_cache }
|
69
|
-
it { is_expected.not_to be_no_store }
|
70
|
-
it { is_expected.not_to be_vary_star }
|
71
|
-
|
72
|
-
describe "#max_age" do
|
73
|
-
subject { cache_headers.max_age }
|
74
|
-
it { is_expected.to eq Float::INFINITY }
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|